Writing CSS In 2023: Is It Any Different Than A Few Years Ago?

Is there anything in the front-end world that’s evolving faster than CSS these days? After what seemed like a long lull following blockbusters Flexbox and Grid, watching CSS release new features over the past few years has been more like watching a wild game of rugby on the telly. The pace is exciting, if not overwhelming at the same time.

But have all these bells and whistles actually changed the way you write CSS? New features have certainly influenced the way I write CSS today, but perhaps not quite as radically as I would have expected.

And while I’ve seen no shortage of blog posts with high-level examples and creative experiments of all these newfangled things that are available to us, I have yet to see practical applications make their way into production or everyday use. I remember when Sass started finding its way into CSS tutorials, often used as the go-to syntax for code examples and snippets. I’m not exactly seeing that same organic adoption happen with, say, logical properties, and we’ve had full browser support for them for about two years now.

This isn’t to rag on anyone or anything. I, for one, am stoked beyond all heck about how CSS is evolving. Many of the latest features are ones we have craved for many, many years. And indeed, there are several of them finding their way into my CSS. Again, not drastically, but enough that I’m enjoying writing CSS more now than ever.

Let me count the ways.

More And More Container Queries

I’ll say it: I’ve never loved writing media queries for responsive layouts. Content responds differently to the width of the viewport depending on the component it’s in. And balancing the content in one component has always been a juggling act with balancing the content in other components, adding up to a mess of media queries at seemingly arbitrary breakpoints. Nesting media queries inside a selector with Sass has made it tolerable, but not to the extent that I “enjoyed” writing new queries and modifying existing ones each time a new design with UI changes is handed to me.

Container queries are the right answer for me. Now I can scope child elements to a parent container and rely on the container’s size for defining where the layout shifts without paying any mind to other surrounding components.

The other thing I like about container queries is that they feel very CSS-y. Defining a container directly on a selector matches a natural property-value syntax and helps me avoid having to figure out math upfront to determine breakpoints.

.parent {
  container-type: inline-size;
}

@container (min-width: 600px) {
  .child {
    align-self: center;
  }
}

I still use media queries for responsive layouts but tend to reserve them for “bigger” layouts that are made up of assembled containers. Breakpoints are more predictable (and can actually more explicitly target specific devices) when there’s no need to consider what is happening inside each individual container.

Learn About Container Queries

Grouping Styles In Layers

I love this way of managing the cascade! Now, if I have a reset or some third-party CSS from a framework or whatever, I can wrap those in a cascade layer and chuck them at the bottom of a file so my own styles are front and center.

I have yet to ship anything using cascade layers, but I now reach for them for nearly every CodePen demo I make. The browser support is there, so that’s no issue. It’s more that I still rely on Sass on projects for certain affordances, and maintaining styles in partialized files still feels nice to me, at least for that sort of work.

But in an isolated demo where all my styles are in one place, like CodePen? Yeah, all the cascade layers, please! Well, all I really need is one layer for the base styles since un-layered styles have higher specificity than layered ones. That leaves my demo-specific styles clean, uncluttered, and still able to override the base at the top, which makes it way more convenient to access them.

body {
  display: grid;
  gap: 3rem;
  place-items: center;
}

@layer base {
  body {
    font-size: 1.25rem;
    line-height: 1.35;
    padding: 3rem;
  }
}

Learn More About Cascade Layers

:is() And :where()

I definitely reach for these newer relational pseudo-selectors, but not really for the benefits of selecting elements conditionally based on relationships.

Instead, I use them most often for managing specificity. But unlike cascade layers, I actually use these in production.

Why? Because with :is(), specificity is determined not by the main selector but by the most specific selector in its argument list.

/* Specificity: 0 1 0 */
:is(ol, .list, ul) li {}

/* Specificity: 0 0 2 */
ol li {}

The .list selector gives the first ruleset a higher specificity score meaning it “beats” the second ruleset even though the first ruleset is higher in the cascade.

On the flip side, the specificity of :where() is a big ol’ score of zero, so it does not add to the overall score of whatever selector it’s on. It simply doesn’t matter at all what’s in its argument list. For the same reason I use :is() to add specificity, I use :where() to strip it out. I love keeping specificity generally low because I still want the cascade to operate with as little friction as possible, and :where() makes that possible, especially for defining global styles.

A perfect example is wrapping :not() inside :where() to prevent :not() from bumping up specificity:

/* Specificity: 0 0 0 */
:where(:not(.some-element)) {}

Taken together, :is() and :where() not only help manage specificity but also take some cognitive load from “naming” things.

I’m one of those folks who still love the BEM syntax. But naming is one of the hardest things about it. I often find myself running out of names that help describe the function of an element and its relationship to elements around it. The specificity-wrangling powers of :is() and :where() means I can rely less on elaborate class names and more on element selectors instead.

Learn More About :is() And :where()

The New Color Function Syntax

The updated syntax for color functions like rgb() and hsl() (and the evolving oklch() and oklab()) isn’t the sort of attention-grabbing headline that leads to oo’s and aw’s, but it sure does make it a lot better to define color values.

For one, I never have to reach for rgba() or hsla() when I need an alpha value. In fact, I always used those whether or not I needed alpha because I didn’t want to bother deciding which version to use.

color: hsl(50deg, 100%, 50%);

/* Same */
color: hsla(50deg, 100%, 50% / 1)

Yes, writing the extra a, /, and 1 was worth the cost of not having to think about which function to use.

But the updated color syntax is like a honey badger: it just doesn’t care. It doesn’t care about the extra a in the function name. It doesn’t even care about commas.

color: hsl(50deg 100% 50% / .5);

So, yeah. That’s definitely changed the way I write colors in CSS.

What I’m really excited to start using is the newer oklch() and oklab() color spaces now that they have full browser support!

Learn More About CSS Color 4 Features

Sniffing Out User Preferences

I think a lot of us were pretty stoked when we got media queries that respect a user’s display preferences, the key one being the user’s preferred color scheme for quickly creating dark and light interfaces.

:root {
  --bg-color: hsl(0deg 0% 100%);
  --text-color: hsl(0deg 0% 0%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: hsl(0deg 0% 0%);
    --text-color: hsl(0deg 0% 100%);
  }
}

body {
  background: var(--bg-color);
  color: var(--text-color);
}

But it’s the prefers-reduced-motion query that has changed my CSS the most. It’s the first thing I think about any time a project involves CSS animations and transitions. I love the idea that a reduced motion preference doesn’t mean nuking all animation, so I’ll often use prefers-reduced-motion to slow everything down when that’s the preference. That means I have something like this (usually in a cascade layer for base styles):

@layer base {
  :root {
    --anim-duration: 1s;
  }

  /* Reduced motion by default */
  body {
    animation-duration: --anim-duration;
    transition: --anim-duration;
  }

  /* Opt into increased motion */
  @media screen and (prefers-reduced-motion: no-preference) {
    body {
      --anim-duration: .25s;
    }
  }
}

Learn More About User Preference Queries

Defining Color Palettes

I’ve used variables for defining and assigning colors ever since I adopted Sass and was thrilled when CSS custom properties came. I’d give generic names to the colors in a palette before passing them into variables with more functional names.

/* Color Palette */
--red: #ff0000;
/* etc. */

/* Brand Colors */
--color-primary: var(--red);
/* etc. */

I still do this, but now I will abstract things even further using color functions on projects with big palettes:

:root {
  /* Primary Color HSL */
  --h: 21deg;
  --s: 100%;
  --l: 50%;

  --color-primary: hsl(var(--h) var(--s) var(--l) / 1);
}

.bg-color {
  background: var(--color-primary);
}

.bg-color--secondary {
  --h: 56deg;
  background: hsl(var(--h) var(--s) var(--l) / 1);
}

A little too abstract? Maybe. But for those projects where you might have ten different varieties of red, orange, yellow, and so on, it’s nice to have this level of fine-grained control to manipulate them. Perhaps there is more control with color-mix() that I just haven’t explored yet.

Learn More About Defining Color Palettes

What I’m Not Using

Huh, I guess I am writing CSS a bit differently than I used to! It just doesn’t feel like it, but that probably has to do with the fact that there are so many other new features I am not currently using. The number of new features I am using is much, much lower than the number of features I have yet to pick up, whether it’s because of browser support or because I just haven’t gotten to it yet.

CSS Nesting

I’m really looking forward to this because it just might be the tipping point where I completely drop Sass for vanilla CSS. It’s waiting for Firefox to support it at the time of this writing, so it could be right around the corner.

Style Queries

I’ve made no secret that applying styles to elements based on the styles of other elements is something that I’m really interested in. That might be more of an academic interest because specific use cases for style queries elude me. Maybe that will change as they gain browser support, and we see a lot more blog posts where smart folks experiment with them.

:has()

I’ll definitely use this when Firefox supports it. Until then, I’ve merely tinkered with it and have enjoyed how others have been experimenting with it. Without full support, though, it hasn’t changed the way I write CSS. I expect that it will, though, because how can having the ability to select a parent element based on the child it contains be a bad thing, right?

Dynamic Viewport Units

I’ve started sprinkling these in my styles since they gained wide support at the end of 2022. Like style queries, I only see limited use cases — most notably when setting elements to full height on a mobile device. So, instead of using height: 100vh, I’m starting to write height: 100dvh more and more. I guess that has influenced how I write CSS, even if it’s subtle.

Media Query Range Syntax

Honestly, I just haven’t thought much about the fact that there’s a nicer way to write responsive media queries on the viewport. I’m aware of it but haven’t made it a part of my everyday CSS for no other reason than ignorance.

OKLCH/OKLAB Color Spaces

oklch() will most definitely be my go-to color function. It gained wide support in March of this year, so I’ve only had a couple of months and no projects to use it. But given the time, I expect it will be the most widely used way to define colors in my CSS.

The only issue with it, I see, is that oklch() is incompatible with another color feature I’m excited about...

color()

It’s widely supported now, as of May 2023! That’s just too new to make its way into my everyday CSS, but you can bet that it will. The ability to tap into any color space — be it sRGB, Display P3, or Rec2020 — is just so much nicer than having to reach for a specific color function, at least for colors in a color space with RGB channels (that’s why color() is incompatible with oklch() and other non-RGB color spaces).

--primary-color: color(display-p3 1 0.4 0);

I’m not in love with RGB values because they’re tough to understand, unlike, say, HSL. I’m sure I’ll still use oklch() or hsl() in most cases for that very reason. It’s a bummer we can’t do something like this:

/* 👎 */
--primary-color: color(oklch 70% 0.222 41.29);

We have to do this instead:

/* 👍 */
--primary-color: oklch(70% 0.222 41.29);

The confusing thing about that is it’s not like Display P3 has its own function like OKLCH:

/* 👎 */
--primary-color: display-p3(1 0.434 0.088);

We’re forced to use color() to tap into Display P3. That’s at odds with OKLCH/OKLAB, where we’re forced to reach for those specific functions.

Maybe one day we’ll have a global color() function that supports them all! Until then, my CSS will use both color() and specific functions like oklch() and decide which is best for whatever I’m working on.

I’ll also toss color-mix() in this bucket, as it gained full support at the same time as color(). It’s not something I write regularly yet, but I’ll certainly play with it, likely for creating color palettes.

Honorable Mentions

It would be quite a feat to comment on every single new CSS feature that has shipped over the past five or so years. The main theme when it comes to which features I am not using in my day-to-day work is that they are simply too new or they lack browser support. That doesn’t mean I won’t use them (I likely will!), but for the time being, I’m merely keeping a side-eye on them or simply having a fun time dabbling in them.

Those include:

  • Trigonometric functions,
  • Anchor position,
  • Scroll-linked animations,
  • initial-letter,
  • <selectmenu> and <popover>,
  • View transitions,
  • Scoped Styles.

What about you? You must be writing CSS differently now than you were five years ago, right? Are you handling the cascade differently? Do you write more vanilla CSS than reaching for a preprocessor? How about typography, like managing line heights and scale? Tell me — or better yet, show me — how you’re CSS-ing these days.

Color Mechanics In UI Kits

I am currently working on research linked to a project on creating a complete guide to developing a UI kit as a technical system. In the first stage, I collected technical decisions, requirements, and possible solutions by analyzing open-source and proprietary UI kits. My initial plan was to to dive deep into every detail after collecting the main decisions that were made in dozens of such UI kits.

At my main workplace, an open-source UI kit is used under the hood. I soon noticed that it was difficult to understand its API when it came to anything related to colors.

I had many questions:

  • Which tasks does the kit’s API solve?
  • Which concepts does its API implement?
  • How is the API implemented?
  • What should I know before starting to implement such mechanics in a UI kit from scratch?
  • What are the best practices?

I decided to temporarily interrupt my data collection and dive deep into this topic. In this article, I want to share some things that I’ve learned. I suppose that I’m not the only one who has such questions, and this article’s goal is to answer these questions to save you time. It will also help me not to burn out and to continue my research work.

How to deal with colors is one of many technical decisions. It incorporates many subdecisions and relates to other decisions, such as:

  • How to implement theme switching — according to user action or the OS setting?
  • How to provide theme configuration for different system levels?
  • How to automatically make a color palette?
  • How to implement color-contrast checking?
  • How to support different contrast models? (Windows has high-contrast mode, whereas macOS has inverted colors.)

In the article, I’ll cover two parts. First, we’ll look at base operations, which include the definition and use of colors and known issues and practices related to color. Secondly, we’ll look into an approach to solving tasks by analyzing existing solutions and understanding the connections between them.

Some code examples will contain Sass and TypeScript, but these aren’t the focus of this article. You will hopefully come to understand a model that you can implement with the tools of your choice.

Also, I’d like to warn you against trying to create your own UI kit. The subdecisions that I mentioned aren’t done consciously. You will see that even implementing a small part of a kit, such as the definition and use of colors, is not as easy as it seems at first glance. Can you imagine the complexity of developing an entire system?

As reference examples, we will use Material UI and Fluent UI React Northstar.

Why them?

As for Material UI:

  • It contains a lot of best practices (I have compared it with others).
  • It’s one of the most popular UI kits in open-source software (at least according to the GitHub stars).
  • I have a lot of experience in using and customizing it.

As for Fluent UI React Northstar:

  • It contains a lot of best practices (I’ve also compared it with others);
  • It’s used in large-scale enterprise projects.
  • It contains new concepts that simplify the public API and implementation based on previous experience developing UI kits (see the Fluent UI Web readme).

As a bonus, you will understand how to use the APIs of these UI kits.

To achieve the article’s goals, we will follow a few steps:

  1. Consider which tasks are required to be solved.
  2. Define the terms and their meaning. Without a common language, it would be hard for us to understand each other.
    “A project faces serious problems when its language is fractured. Domain experts use their jargon, while technical team members have their own language tuned for discussing the domain in terms of design.

    The terminology of day-to-day discussions is disconnected from the terminology embedded in the code (ultimately the most important product of a software project). And even the same person uses different language in speech and in writing so that the most incisive expressions of the domain often emerge in a transient form that is never captured in the code or even in writing.

    Translation blunts communication and makes knowledge crunching anemic.

    Domain-Driven Design Tackling Complexity in the Heart of Software, Eric Evans, Addison-Wesley, 2004
  3. Consider problems we might encounter and how to solve them.
  4. Illustrate solutions by considering the implementation of reference UI kits.
  5. Follow the example of the best reference.

Let’s dive in!

Colors Mechanics Model

Terminology

Let’s say that our ultimate goal is to provide the ability to switch themes. In this case, the following concepts come into play:

  • Color, hue
    This refers to the type of color (red, blue, and so on). The term we’ll use in this article is “color”.
  • Color shade, color gradient, color variant, color tone
    Color may be determined by hue, brightness, and saturation. The term we’ll use in this article is “color variant”.
“One important detail about Munsell’s color system is that he divided the color space into three new dimensions: The hue determined the type of color (red, blue, and so on), the value determined the brightness of the color (light or dark), and the chroma determined the saturation of the color (the purity of the color). These dimensions are still used to this day in some representations of the RGB color model.”

— “A Short History of Color Theory”, from Programming Design Systems
  • Color palette
    This is a set of variants of color. We’ll refer to it in this article as “color palette”.
  • Design tokens
    These are general component property names from a design point of view. The term we’ll use in this article is “visual properties”. For example:
    • border,
    • text,
    • background.
  • Color scheme, theme, color theme
    A color scheme is created to impose some constraints. The term we’ll use in this article is “color scheme” because “theme” is more general than “color scheme” (encompassing font size and so on). For example, a color scheme might:
    • contain only variants of the color pink,
    • be tailored to light or dark illumination of the space around the device,
    • be tailored to people with vision impairment,
    • be tailored to specific device constraints.

Producing Operations With Color

We’ll consider basic operations such as defining and using color.

Color is defined according to various color model notations (RGB, HSL, etc.).

In the case of development of a digital user interface, a color scheme is created to color UI components. Each UI component might have a different color for its various properties in each scheme. For example, the background color of a call-to-action button might be red or blue depending on the current theme.

So, how can colors be represented?

How to Name Variables?

If you just name variables according to their color, you will get into a situation where a variable named redColor should have a blue color value in another scheme.

Also, the components that show an error state should still be able to use the red color from the redColor variable. So, another layer of abstraction needs to be introduced to solve the problem.

This additional layer organizes colors by their function (for example, error state) or visual property name (for example, background). It acts as a color scheme.

It’s interesting that organization by function was already introduced to CSS properties.

Each value in the layer’s structure would be mapped to the color palette value by color name and color variant.

How To Remember Use Cases?

After adding colors to the layer, you might encounter a minor problem — how to remember their use cases:

I remember the very first time I tried Sass on a project. The first thing I wanted to do was variablize my colors. From my naming-things-in-HTML skillz, I knew to avoid classes like .header-blue-left-bottom because the color and position of that element might change. It’s better for them to reflect what it is than what it looks like.

So, I tried to make my colors semantic, in a sense — what they represent, not what they literally are:

$mainBrandColor: #F060D6;
$secondaryFocus: #4C9FEB;
$fadedHighlight: #F1F3F4;
But I found that I absolutely never remembered them and had to constantly refer to where I defined them in order to use them. Later, in a ‘screw it’ moment, I named colors more like…

$orange: #F060D6;
$red: #BB532E;
$blue: #4C9FEB;

$gray-1: #eee;
$gray-2: #ccc;
$gray-3: #555;
I found that to be much more intuitive, with little, if any, negative side effects. After all, this isn’t crossing the HTML-CSS boundary here; this is all within CSS and developer-only-facing, which puts more of a narrow scope on the problem.”

— Chris Coyier, “What do you name color variables?

In the initial stage of the project, writing comments next to the variables might help. And creating a dictionary might help to communicate with a design team in subsequent stages.

“The use of dictionaries as a means to establish a common understanding of terms has already proved its benefits in various software-related fields. Literature on software project management recommends the usage of a project glossary or dictionary that contains a description of all terms used in a project. This glossary serves as a reference for project participants over the entire project life cycle.”

Concise and Consistent Naming, Florian Deissenboeck, Markus Pizka, 2006, Software Qual J

Now we understand why just using color names wouldn’t work. But it points to the solution for another minor problem: defining names for variants of a particular color.

How To Define Names For Color Variants?

The solution is simple. Just add numbers as suffixes to the names. The advantage of this approach is that adding a new color will be easy, and the suffix will tell you that the color is a variant of another color. But this is still hard to remember.

$gray-1: #eee;
$gray-2: #ccc;
$gray-3: #555;

Another approach is to give a unique name to each color. This approach is the least convenient because names wouldn’t have any useful information, and you would have to remember them. You would need to define the names or use a name generator, which is an unnecessary dependency.

A better solution is suggested by Zain Adeel in his article “My Struggle With Colors”:

Using a scale from 10–100 with a tone at each ten is by far the simplest. A purple-10 will understandably be the lighter tone in comparison to a purple-50. The familiarity of such an approach allows the system to grow predictably.

The approach provides maximum useful information by name. Also, it can cover more cases if a prefix is added. For example, the prefix “A” can denote an accent color. As explained in the Material UI documentation:

A single color within the palette is made up of a hue, such as “red”, and shade, such as “500”. “red 50” is the lightest shade of red (pink!), while “red 900” is the darkest. In addition, most hues come with “accent” shades, prefixed with an A.

A disadvantage is that the cascade will change if you ever have to add an intermediate color with a brightness variant. For example, if you have to add a color between gray-10 and gray-20, then you might replace gray-20 and then have to adjust the following color values (gray-30, gray-40, and so on).

Also, any solution comes with potential maintenance issues. For example, we would have to ensure that all color definitions have all possible variants in order to avoid a scenario where we have gray-20 but not red-20.

One approach to solving problems is Material Design’s color system. One of the values of this guide is that it doesn’t contain details of technical implementation, but rather focuses on concepts containing only important information.

Illustrating Solutions

Let’s look at an implementation from top to bottom.

Fluent UI React Northstar (@fluentui/react-northstar@0.63.1)

Color Scheme

Let’s consider the “Teams” theme.

Fluent UI React Northstar has a two-dimensional color scheme model.

“Brand” is the color scheme. “Light theme,” “HC theme,” and “Dark theme” will also be color schemes in this article.

Grouping Approach

Color scheme object keys are visual properties combined with states.

export const colorScheme: ColorSchemeMapping<ColorScheme, TeamsColorNames> = {
  amethyst: createColorScheme({
    background: colors.amethyst[600],
    backgroundHover: colors.amethyst[700],
    backgroundHover1: colors.amethyst[500],
    backgroundActive: colors.amethyst[700],
  }),
};

Note: Check out the source code.

In the siteVariables key of the theme configuration, the colors palette is located in the colors key, and the color scheme is in the colorScheme key. They are explicitly separated.

Color Palette

A color palette is an object. Interestingly, some color values are defined with transparency, and the palette contains colors named according to their function.

export const colors: ColorPalette<TeamsTransparentColors> = {
  ...contextualAndNaturalColors,
  ...primitiveColors,
  ...transparentColors,
};

Note: Check out the source code.

“Colors in Teams color palette have the following categorization.

Primitive colors

This part of the palette contains colors that, semantically, cannot have any tints. This group is represented by two colors, black and white — as there is nothing blacker than black and nothing whiter than white.

[...]

Natural colors

This part of the palette includes colors from those that are the most commonly used among popular frameworks (blue, green, gray, orange, pink, purple, teal, red, yellow). Each color includes at least ten gradients; this allows us to satisfy the most common needs.

This decision is experienced from Material UI and allows us to define more variants than by using semantical naming (lightest, lighter, etc.). However, there is no requirement for a client to define all the gradient values for each color — it is just enough to define those that are actually used in the app.

[...]

Contextual colors

This part of the palette may include brand color as well as danger, success, info colors, and so on.”

— “Colors”, Fluent UI documentation

The value in the object’s key by color name may be an object containing keys such as a color variant or just a color string literal of a specific color model.

export const naturalColors: TeamsNaturalColors = {
  orange: {
    50: '#F9ECEA', // darkOrange[50]
    100: '#EFDBD3', // app orange14
    200: '#EDC2A7', // old message highlight border
    300: '#E97548', // orange[900]
    400: '#CC4A31', // app orange04 darkOrange[400]
    500: '#BD432C', // app orange03
    600: '#A33D2A', // app orange02
    700: '#833122', // app orange01 darkOrange[900]
    800: '#664134', // app orange14 dark
    900: '#51332C', // app orange16 dark
  },
}

Note: Check out the source code.

export const primitiveColors: PrimitiveColors = {
  black: ‘#000’,
  white: ‘#fff’,
};

Note: Check out the source code.

Material UI (@mui/material@5.10.4)

Color Scheme

Material UI provides only dark and light color schemes as default schemes.

Grouping Approach

The palette key of the theme configuration contains the color scheme used in this article.

Keys linked to the colors of the color scheme have been chosen according to the following groups:

  1. The functional purpose of the color:
    • primary
    • primaryDark
    • text
    • gray
    • error
    • success
    • warning
    • secondary
    • info
    • action
    • divider

    As the value in these object keys, they may be the following keys:
    • light
    • main
    • dark
    • contrastText
  2. Visual property name
    For example, background.
  3. Colors grouped in a category:
    {
      common: {
        black: "#1D1D1D"
        white: "#fff"
      }
    }
    

At the same time, the values in theme.palette contain other stuff:

  • The current color scheme mode:
    {
      mode: 'dark',
    }
  • Utilities such as getContrastText,
  • ...and more.

Color Palette

Each color is an object. Keys are a color variant. The prefix A denotes the accent color.

const blue = {
  50: '#e3f2fd',
  100: '#bbdefb',
  200: '#90caf9',
  300: '#64b5f6',
  400: '#42a5f5',
  500: '#2196f3',
  600: '#1e88e5',
  700: '#1976d2',
  800: '#1565c0',
  900: '#0d47a1',
  A100: '#82b1ff',
  A200: '#448aff',
  A400: '#2979ff',
  A700: '#2962ff',
};

export default blue;

Note: Check out the source code.

Comparison

We will choose the best reference example according to the following factors:

  • an API that corresponds with the given terminology agreed on by client;
  • implementation that corresponds with the given terminology;
  • following best practices for the designated tasks.

Correspondence With Given Terminology

Fluent UI React Northstar

Pros:

  • The color palette and color scheme are explicitly separated.

Cons:

  • The color palette contains not only common color names (red, green, and so on).

Material UI

Pros:

  • The color scheme (the “palette” key in the theme configuration) contains not only colors.
  • The “palette” key name is confusing because if you want to use a color palette, you would import the “colors” object from the @mui/material package.
  • Misunderstanding is compounded by incomplete compliance with the Material UI guide:

Used Practices

From the point of view of this factor, let’s consider only the differences.

Fluent UI React Northstar

Adding a postfix denoting the brightness of color was chosen as the approach to name variables. The color palette contains colors named by their function and common color names (red, green, and so on). The color scheme groups color by visual properties combined with states.

Material UI

Adding a suffix denoting the brightness of the color and a prefix denoting the accent color was decided on as the approach to naming variables. The color palette contains colors named by their common color names (red, green, and so on). The color scheme groups color by visual properties and function.

I would use the Fluent UI React Northstar as the reference for implementation because it accords with the given terminology. If the topics that were mentioned in the introduction as not being considered were to be considered, then the choice might have been different.

Conclusion

Let’s summarize the key points:

  1. If you want to implement something, examine the best references in order to avoid reinventing the wheel, and focus instead on finding solutions to unresolved problems.
  2. During the examination process, you will encounter solved tasks and terms. Make a summary of them.
  3. Choose the best solutions according to your task’s requirements and limitations.
  4. Choose the best reference that corresponds with the solutions that you chose.
  5. Implement by following the best reference.

If you want to dig into color theory, I strongly recommend the book Programming Design Systems, written by Rune Skjoldborg Madsen.

I would like to thank Andrey Antropov, Daniyal Gabitov, and Oleksandr Fediashov for their suggestions for improvement and valuable additions. I would also like to thank the editors of Smashing Magazine for their assistance.

Collective #746




Our Sponsor

Where is my website traffic and how do I get more?

Some straight facts about search engine traffic: 51% of all website traffic is driven by SEO, 40% of all online revenue is driven by SEO, 72% of search traffic goes to the first 10 results on the first page. We have the formula to get your website the conversions you are looking for! Book your complimentary SEO call.

Book now





Copy Dennis

Dennis Snellenberg created this homnage to all the creators who have taken a little bit too much inspiration from his portfolio website.

Check it out







MeshEpoxyMaterial

Paul Henschel has created a demo that showcases the capabilities of the new MeshTransmissionMaterial in drei, which allows for realistic rendering of glass, gelatin, or epoxy materials with RGB shift and noise effects.

Check it out





Sailboat UI

This modern UI component library, built with Tailwind CSS, provides over 150 pre-designed components to help developers quickly and easily create websites.

Check it out




Shift Happens

“Shift Happens” is a book that traces the history of keyboards from their early days as typewriters to the present-day digital versions. The site shows a wonderful 3D preview of the book!

Check it out





Our Sponsor

Explore the world like a local with Babbel

If you’ve always wanted to learn a new language, Babbel will be the most productive 10 minutes of your day. Trusted by over 10 million subscribers worldwide, the subscription-based language learning platform can get you confidently conversing in a new tongue in just three weeks. This isn’t your grade school textbook: the bite-sized lessons, available in 14 languages, teach you localized vocabulary you’ll actually use in the real world. Plus, speech recognition technology helps you perfect your accent. Sign up today and get up to 60 percent off your subscription.

Learn more

Collective #736




Our sponsor

Black Friday is coming early!

Save the date and win an iMac at this year’s Black Friday sale! Purchase any Divi product & get our biggest discount of all time, a free prize, a pack of website templates and exclusive access to dozens of additional product deals.

Get notified



Lucia

Lucia is a simple yet flexible user and session management library that provides an abstraction layer between your app and your database.

Check it out




The New CSS Media Query Range Syntax

The Media Queries Level 4 specification has introduced a new syntax for targeting a range of viewport widths using common mathematical comparison operators, like , and =, that make more sense syntactically while writing less code for responsive web design.

Read it







Outstatic

An open source static site CMS for Next.js. Create your blog or website in minutes. No dabatase needed.

Check it out



Image Steganography Tool

A simple C++ encryption and steganography tool that uses Password-Protected-Encryption to secure a file’s contents, and then proceeds to embed it insde an image’s pixel-data using Least-Significant-Bit encoding.

Check it out


Hydra

Hydra is live code-able video synth and coding environment that runs directly in the browser. It is free and open-source and made for beginners and experts alike.

Check it out






Swurl

Swurl is a new search engine that allows you to search everything instantly!

Check it out





TamoTam

This project is a combination of a personal education project to learn JavaScript Mobile Development using React Native and a business idea to create a mobile app with offline-only events.

Check it out


Meet Skeleton: Svelte + Tailwind For Reactive UIs

If you’ve ever found yourself tasked with creating and implementing custom UI, then you know how difficult it can be to meet the demands of the modern web. Your interface must be responsive, reactive, and accessible, all while remaining visually appealing to a broad spectrum of users. Let’s face it; this can be a challenge for even the most seasoned frontend developer.

Over the last ten years, we’ve seen the introduction of UI frameworks that help ease this burden. Most rely on JavaScript and lean into components and reactive patterns to handle real-time user interaction. Frameworks such as Angular, React, and Vue have been established as the standard for what we currently know as modern frontend development.

Alongside the tools, we’ve seen the rise of framework-specific libraries like Angular Material, Mantine (for React), and Vuetify that to provide a “batteries included” approach to implementing UI, including deep integration of each framework’s unique set of features. With the emergence of new frameworks such as Svelte, we might expect to see similar libraries appear to fulfill this role. To gain insight into how these tools might work, let’s review what Svelte brings to frontend development.

Svelte And SvelteKit

In 2016, Rich Harris introduced Svelte, a fresh take on components for the web. To understand the benefits of Svelte, see his 2019 conference talk titled “Rethinking Reactivity,” where Rich explains the origins of Svelte and demonstrates its unique compiler-driven approach.

Skeleton was founded by the development team at Brain & Bones. The team, myself included, has been consistently impressed with Svelte and the tools it brings to the frontend developer’s arsenal. The team and I were looking to migrate several internal projects from Angular to SvelteKit when we realized there was an opportunity to combine Svelte’s intuitive component system with the utility-driven design systems of Tailwind, and thus Skeleton was born.

The team realized Skeleton has the potential to benefit many in the Svelte community, and as such, we’ve decided to make it open-source. We hope to see Skeleton grow into a powerful UI toolkit that can help many developers, whether your skills lie within the frontend space or not.

To see what we mean, let’s take a moment to create a basic SvelteKit app and integrate Skeleton.

Getting Started With Skeleton

Open your terminal and run each of the following commands. Be sure to set “my-skeleton-app” to whatever name you prefer. When prompted, we recommend using Typescript and creating a barebones (aka “skeleton”) project:

npm create svelte@latest my-skeleton-app
cd my-skeleton-app
npm install
npm run dev -- --open

This will generate the SvelteKit app, move your terminal into the project directory, install all required dependencies, then start a local dev server. Using the -- --open flag here will open the following address in your browser automatically:

http://localhost:5173/

In your terminal, use Ctrl + C to close and stop the server. Don’t worry; we’ll resume it in a moment.

Next, we need to install Tailwind. Svelte-add helps make this process trivial. Simply run the following commands, and it’ll handle the rest.

npx svelte-add@latest tailwindcss
npm install

This will install the latest Tailwind version into your project, create /src/app.css to house your global CSS, and generate the necessary tailwind.config.cjs. Then we install our new Tailwind dependency.

Finally, let’s install the Skeleton package via NPM:

npm i @brainandbones/skeleton --save-dev

We’re nearly ready to add our first component, and we just need to make a couple of quick updates to the project configuration.

Configure Tailwind

To ensure Skeleton plays well with Tailwind, open tailwind.config.cjs in the root of your project and add the following:

module.exports = {
    content: [
        // ...
        './node_modules/@brainandbones/skeleton/*/.{html,js,svelte,ts}'
    ],
    plugins: [
        require('@brainandbones/skeleton/tailwind.cjs')
    ]
}

The content section ensures the compiler is aware of all Tailwind classes within our Skeleton components, while plugins uses a Skeleton file to prepare for the theme we’ll set up in the next section.

Implement A Skeleton Theme

Skeleton includes a simple yet powerful theme system that leans into Tailwind’s best practices. The theme controls the visual appearance of all components and intelligently adapts for dark mode while also providing access to Tailwind utility classes that represent your theme’s unique color palette.

The Skeleton team has provided a curated set of themes, as well as a theme generator to help design custom themes using either Tailwind colors or hex colors to match your brand’s identity.

To keep things simple, we’ll begin with Skeleton’s default theme. Copy the following CSS into a new file in /src/theme.css.

:root {
    /* --- Skeleton Theme --- */
    /* primary (emerald) */
    --color-primary-50: 236 253 245;
    --color-primary-100: 209 250 229;
    --color-primary-200: 167 243 208;
    --color-primary-300: 110 231 183;
    --color-primary-400: 52 211 153;
    --color-primary-500: 16 185 129;
    --color-primary-600: 5 150 105;
    --color-primary-700: 4 120 87;
    --color-primary-800: 6 95 70;
    --color-primary-900: 6 78 59;
    /* accent (indigo) */
    --color-accent-50: 238 242 255;
    --color-accent-100: 224 231 255;
    --color-accent-200: 199 210 254;
    --color-accent-300: 165 180 252;
    --color-accent-400: 129 140 248;
    --color-accent-500: 99 102 241;
    --color-accent-600: 79 70 229;
    --color-accent-700: 67 56 202;
    --color-accent-800: 55 48 163;
    --color-accent-900: 49 46 129;
    /* warning (rose) */
    --color-warning-50: 255 241 242;
    --color-warning-100: 255 228 230;
    --color-warning-200: 254 205 211;
    --color-warning-300: 253 164 175;
    --color-warning-400: 251 113 133;
    --color-warning-500: 244 63 94;
    --color-warning-600: 225 29 72;
    --color-warning-700: 190 18 60;
    --color-warning-800: 159 18 57;
    --color-warning-900: 136 19 55;
    /* surface (gray) */
    --color-surface-50: 249 250 251;
    --color-surface-100: 243 244 246;
    --color-surface-200: 229 231 235;
    --color-surface-300: 209 213 219;
    --color-surface-400: 156 163 175;
    --color-surface-500: 107 114 128;
    --color-surface-600: 75 85 99;
    --color-surface-700: 55 65 81;
    --color-surface-800: 31 41 55;
    --color-surface-900: 17 24 39;
}

Note: Colors are converted from Hex to RGB to properly support Tailwind’s background opacity.

Next, let’s configure SvelteKit to use our new theme. To do this, open your root layout file at /src/routes/__layout.svelte. Declare your theme just before your global stylesheet app.css.

import '../theme.css'; // <--
import '../app.css';

To make things look a bit nicer, we’ll add some basic <body> element styles that support either light or dark mode system settings. Add the following to your /src/app.css.

body { @apply bg-surface-100 dark:bg-surface-900 text-black dark:text-white p-4; }

For more instruction, consult the Style documentation which covers global styles in greater detail.

Add A Component

Finally, let’s implement our first Skeleton component. Open your app’s home page /src/routes/index.svelte and add the follow. Feel free to replace the file’s entire contents:

<script lang="ts">
    import { Button } from '@brainandbones/skeleton';
</script>

<Button variant="filled-primary">Skeleton</Button>

To preview this, we’ll need to restart our local dev server. Run npm run dev in your terminal and point your browser to http://localhost:5173/. You should see a Skeleton Button component appear on the page!

Customizing Components

As with any Svelte component, custom “props” (read: properties) can be provided to configure your component. For example, the Button component’s variant prop allows us to set any number of canned options that adapt to your theme. By switching the variant value to filled-accent we’ll see the button change from our theme’s primary color (emerald) to the accent color (indigo).

Each component provides a set of props for you to configure as you please. See the Button documentation to try an interactive sandbox where you can test different sizes, colors, etc.

You may notice that many of the prop values resembled Tailwind class names. In fact, this is exactly what these are! These props are provided verbatim to the component’s template. This means we can set a component’s background style to any theme color, any Tailwind color, or even set a one-off color using Tailwind’s arbitrary value syntax.

<!-- Using our theme color -->
<Button background="bg-accent-500">Accent</Button>

<!-- Using Tailwind colors -->
<Button background="bg-orange-500">Orange</Button>

<!-- Using Tailwind's arbitrary value syntax -->
<Button background="bg-[#BADA55]">Arbitrary</Button>

This gives you the control to maintain a cohesive set of styles or choose to “draw outside of the lines” with arbitrary values. You’re not limited to the default props, though. You can provide any valid CSS classes to a component using a standard class attribute:

<Button variant="filled-primary" class="py-10 px-20">Big!</Button>

Form Meets Function

One of the primary benefits of framework-specific libraries like Skeleton is the potential for deep integration of the framework’s unique set of features. To see how Skeleton integrates with Svelte, let’s try out Skeleton’s dialog system.

First, add the Dialog component within the global scope of your app. The easiest way to do this is to open /src/routes/__layout.svelte and add the following above the <slot /> element:

<script lang="ts">
    // ...
    import { Dialog } from '@brainandbones/skeleton';
</script>

<!-- Add the Dialog component here -->
<Dialog />

<slot />

Note: The Dialog component will not be visible on the page by default.

Next, let’s update our home page to trigger our first Dialog. Open /src/routes/index.svelte and replace the entire contents with the following:

<script lang="ts">
    import { Button, dialogStore } from '@brainandbones/skeleton';
    import type { DialogAlert } from '@brainandbones/skeleton/Notifications/Stores';

    function triggerDialog(): void {
        const d: DialogAlert = {
            title: ‘Welcome to Skeleton.’,
            body: ‘This is a standard alert dialog.’,
        };
        dialogStore.trigger(d);
    }
</script>

<Button variant="filled-primary" on:click={() => { triggerDialog() }}>Trigger Dialog</Button>

This provides all the scaffolding needed to trigger a dialog. In your browser, click the button, and you should see your new dialog message appear!

Skeleton accomplishes this using Svelte’s writable stores, which are reactive objects that help manage the global state. When the button is clicked, the dialog store is triggered, and an instance of a dialog is provided to the store. The store then acts as a queue. Since stores are reactive, this means our Dialog component can listen for any updates to the store’s contents. When a new dialog is added to the queue, the Dialog component updates to show the contents on the screen.

Skeleton always shows the top-most dialog in the queue. When dismissed, it then displays the following dialog in the queue. If no dialogs remain, the Dialog component hides and returns to its default non-visible state.

Here’s a simple mock to help visualize the data structure of the dialog store queue:

dialogStore = [
    // dialog #1, <-- top items the queue, shown on screen
    // dialog #2, <-- the next dialog in line
    // dialog #3, <-- bottom of the queue, the last added
];

It’s Skeleton’s tight integration with Svelte features that makes this possible. That’s the power of framework-specific tooling — structure, design, and functionality all in one tightly coupled package!

Learn More About Skeleton

Skeleton is currently available in early access beta, but feel free to visit our documentation if you would like to learn more. The site provides detailed guides to help get started and covers the full suite of available components and utilities. You can report issues, request walkthroughs, or contribute code at Skeleton’s GitHub. You’re also welcome to join our Discord community to chat with contributors and showcase projects you’ve created with Skeleton.

Skeleton was founded by Brain & Bones. We feed gamers’ love for competition, providing a platform that harnesses the power of hyper-casual games to enhance engagement online and in-person.

Further Resources

Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D

We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use any shadows. We also combined them with CSS variables and calc() to optimize the code and make it easy to manage.

In this article, we will build off those two articles to create even more complex CSS hover animations. We’re talking about background clipping, CSS masks, and even getting our feet wet with 3D perspectives. In other words, we are going to explore advanced techniques this time around and push the limits of what CSS can do with hover effects!

Cool Hover Effects series:

  1. Cool Hover Effects That Use Background Properties
  2. Cool Hover Effects That Use CSS Text Shadow
  3. Cool Hover Effects That Use Background Clipping, Masks, and 3D (you are here!)

Here’s just a taste of what we’re making:

Hover effects using background-clip

Let’s talk about background-clip. This CSS property accepts a text keyword value that allows us to apply gradients to the text of an element instead of the actual background.

So, for example, we can change the color of the text on hover as we would using the color property, but this way we animate the color change:

All I did was add background-clip: text to the element and transition the background-position. Doesn’t have to be more complicated than that!

But we can do better if we combine multiple gradients with different background clipping values.

In that example, I use two different gradients and two values with background-clip. The first background gradient is clipped to the text (thanks to the text value) to set the color on hover, while the second background gradient creates the bottom underline (thanks to the padding-box value). Everything else is straight up copied from the work we did in the first article of this series.

How about a hover effect where the bar slides from top to bottom in a way that looks like the text is scanned, then colored in:

This time I changed the size of the first gradient to create the line. Then I slide it with the other gradient that update the text color to create the illusion! You can visualize what’s happening in this pen:

We’ve only scratched the surface of what we can do with our background-clipping powers! However, this technique is likely something you’d want to avoid using in production, as Firefox is known to have a lot of reported bugs related to background-clip. Safari has support issues as well. That leaves only Chrome with solid support for this stuff, so maybe have it open as we continue.

Let’s move on to another hover effect using background-clip:

You’re probably thinking this one looks super easy compared to what we’ve just covered — and you are right, there’s nothing fancy here. All I am doing is sliding one gradient while increasing the size of another one.

But we’re here to look at advanced hover effects, right? Let’s change it up a bit so the animation is different when the mouse cursor leaves the element. Same hover effect, but a different ending to the animation:

Cool right? let’s dissect the code:

.hover {
  --c: #1095c1; /* the color */

  color: #0000;
  background: 
    linear-gradient(90deg, #fff 50%, var(--c) 0) calc(100% - var(--_p, 0%)) / 200%, 
    linear-gradient(var(--c) 0 0) 0% 100% / var(--_p, 0%) no-repeat,
    var(--_c, #0000);
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  transition: 0s, color .5s, background-color .5s;
}
.hover:hover {
  color: #fff;
  --_c: var(--c);
  --_p: 100%;
  transition: 0.5s, color 0s .5s, background-color 0s .5s;
}

We have three background layers — two gradients and the background-color defined using --_c variable which is initially set to transparent (#0000). On hover, we change the color to white and the --_c variable to the main color (--c).

Here’s what is happening on that transition: First, we apply a transition to everything but we delay the color and background-color by 0.5s to create the sliding effect. Right after that, we change the color and the background-color. You might notice no visual changes because the text is already white (thanks to the first gradient) and the background is already set to the main color (thanks to the second gradient).

Then, on mouse out, we apply an instant change to everything (notice the 0s delay), except for the color and background-color that have a transition. This means that we put all the gradients back to their initial states. Again, you will probably see no visual changes because the text color and background-color already changed on hover.

Lastly, we apply the fading to color and a background-color to create the mouse-out part of the animation. I know, it may be tricky to grasp but you can better visualize the trick by using different colors:

Hover the above a lot of times and you will see the properties that are animating on hover and the ones animating on mouse out. You can then understand how we reached two different animations for the same hover effect.

Let’s not forget the DRY switching technique we used in the previous articles of this series to help reduce the amount of code by using only one variable for the switch:

.hover {
  --c: 16 149 193; /* the color using the RGB format */

  color: rgb(255 255 255 / var(--_i, 0));
  background:
    /* Gradient #1 */
    linear-gradient(90deg, #fff 50%, rgb(var(--c)) 0) calc(100% - var(--_i, 0) * 100%) / 200%,
    /* Gradient #2 */
    linear-gradient(rgb(var(--c)) 0 0) 0% 100% / calc(var(--_i, 0) * 100%) no-repeat,
    /* Background Color */
    rgb(var(--c)/ var(--_i, 0));
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  --_t: calc(var(--_i,0)*.5s);
  transition: 
    var(--_t),
    color calc(.5s - var(--_t)) var(--_t),
    background-color calc(.5s - var(--_t)) var(--_t);
}
.hover:hover {
  --_i: 1;
}

If you’re wondering why I reached for the RGB syntax for the main color, it’s because I needed to play with the alpha transparency. I am also using the variable --_t to reduce a redundant calculation used in the transition property.

Before we move to the next part here are more examples of hover effects I did a while ago that rely on background-clip. It would be too long to detail each one but with what we have learned so far you can easily understand the code. It can be a good inspiration to try some of them alone without looking at the code.

I know, I know. These are crazy and uncommon hover effects and I realize they are too much in most situations. But this is how to practice and learn CSS. Remember, we pushing the limits of CSS hover effects. The hover effect may be a novelty, but we’re learning new techniques along the way that can most certainly be used for other things.

Hover effects using CSS mask

Guess what? The CSS mask property uses gradients the same way the background property does, so you will see that what we’re making next is pretty straightforward.

Let’s start by building a fancy underline.

I’m using background to create a zig-zag bottom border in that demo. If I wanted to apply an animation to that underline, it would be tedious to do it using background properties alone.

Enter CSS mask.

The code may look strange but the logic is still the same as we did with all the previous background animations. The mask is composed of two gradients. The first gradient is defined with an opaque color that covers the content area (thanks to the content-box value). That first gradient makes the text visible and hides the bottom zig-zag border. content-box is the mask-clip value which behaves the same as background-clip

linear-gradient(#000 0 0) content-box

The second gradient will cover the whole area (thanks to padding-box). This one has a width that’s defined using the --_p variable, and it will be placed on the left side of the element.

linear-gradient(#000 0 0) 0 / var(--_p, 0%) padding-box

Now, all we have to do is to change the value of --_p on hover to create a sliding effect for the second gradient and reveal the underline.

.hover:hover {
  --_p: 100%;
  color: var(--c);
}

The following demo uses with the mask layers as backgrounds to better see the trick taking place. Imagine that the green and red parts are the visible parts of the element while everything else is transparent. That’s what the mask will do if we use the same gradients with it.

With such a trick, we can easily create a lot of variation by simply using a different gradient configuration with the mask property:

Each example in that demo uses a slightly different gradient configuration for the mask. Notice, too, the separation in the code between the background configuration and the mask configuration. They can be managed and maintained independently.

Let’s change the background configuration by replacing the zig-zag underline with a wavy underline instead:

Another collection of hover effects! I kept all the mask configurations and changed the background to create a different shape. Now, you can understand how I was able to reach 400 hover effects without pseudo-elements — and we can still have more!

Like, why not something like this:

Here’s a challenge for you: The border in that last demo is a gradient using the mask property to reveal it. Can you figure out the logic behind the animation? It may look complex at first glance, but it’s super similar to the logic we’ve looked at for most of the other hover effects that rely on gradients. Post your explanation in the comments!

Hover effects in 3D

You may think it’s impossible to create a 3D effect with a single element (and without resorting to pseudo-elements!) but CSS has a way to make it happen.

What you’re seeing there isn’t a real 3D effect, but rather a perfect illusion of 3D in the 2D space that combines the CSS background, clip-path, and transform properties.

Breakdown of the CSS hover effect in four stages.
The trick may look like we’re interacting with a 3D element, but we’re merely using 2D tactics to draw a 3D box

The first thing we do is to define our variables:

--c: #1095c1; /* color */
--b: .1em; /* border length */
--d: 20px; /* cube depth */

Then we create a transparent border with widths that use the above variables:

--_s: calc(var(--d) + var(--b));
color: var(--c);
border: solid #0000; /* fourth value sets the color's alpha */
border-width: var(--b) var(--b) var(--_s) var(--_s);

The top and right sides of the element both need to equal the --b value while the bottom and left sides need to equal to the sum of --b and --d (which is the --_s variable).

For the second part of the trick, we need to define one gradient that covers all the border areas we previously defined. A conic-gradient will work for that:

background: conic-gradient(
  at left var(--_s) bottom var(--_s),
  #0000 90deg,var(--c) 0
 ) 
 0 100% / calc(100% - var(--b)) calc(100% - var(--b)) border-box;
Diagram of the sizing used for the hover effect.

We add another gradient for the third part of the trick. This one will use two semi-transparent white color values that overlap the first previous gradient to create different shades of the main color, giving us the illusion of shading and depth.

conic-gradient(
  at left var(--d) bottom var(--d),
  #0000 90deg,
  rgb(255 255 255 / 0.3) 0 225deg,
  rgb(255 255 255 / 0.6) 0
) border-box
Showing the angles used to create the hover effect.

The last step is to apply a CSS clip-path to cut the corners for that long shadow sorta feel:

clip-path: polygon(
  0% var(--d), 
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, 
  0% 100%
)
Showing the coordinate points of the three-dimensional cube used in the CSS hover effect.

That’s all! We just made a 3D rectangle with nothing but two gradients and a clip-path that we can easily adjust using CSS variables. Now, all we have to do is to animate it!

Notice the coordinates from the previous figure (indicated in red). Let’s update those to create the animation:

clip-path: polygon(
  0% var(--d), /* reverses var(--d) 0% */
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, /* reverses 100% calc(100% - var(--d)) */ 
  0% 100% /* reverses var(--d) calc(100% - var(--d)) */
)

The trick is to hide the bottom and left parts of the element so all that’s left is a rectangular element with no depth whatsoever.

This pen isolates the clip-path portion of the animation to see what it’s doing:

The final touch is to move the element in the opposite direction using translate — and the illusion is perfect! Here’s the effect using different custom property values for varying depths:

The second hover effect follows the same structure. All I did is to update a few values to create a top left movement instead of a top right one.

Combining effects!

The awesome thing about everything we’ve covered is that they all complement each other. Here is an example where I am adding the text-shadow effect from the second article in the series to the background animation technique from the first article while using the 3D trick we just covered:

The actual code might be confusing at first, but go ahead and dissect it a little further — you’ll notice that it’s merely a combination of those three different effects, pretty much smushed together.

Let me finish this article with a last hover effect where I am combining background, clip-path, and a dash of perspective to simulate another 3D effect:

I applied the same effect to images and the result was quite good for simulating 3D with a single element:

Want a closer look at how that last demo works? I wrote something up on it.

Wrapping up

Oof, we are done! I know, it’s a lot of tricky CSS but (1) we’re on the right website for that kind of thing, and (2) the goal is to push our understanding of different CSS properties to new levels by allowing them to interact with one another.

You may be asking what the next step is from here now that we’re closing out this little series of advanced CSS hover effects. I’d say the next step is to take all that we learned and apply them to other elements, like buttons, menu items, links, etc. We kept things rather simple as far as limiting our tricks to a heading element for that exact reason; the actual element doesn’t matter. Take the concepts and run with them to create, experiment with, and learn new things!


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks. You should get the newsletter.

Collective #690







Collective 690 item image

Pglet

Build web apps like a frontend pro in the language you already know. No knowledge of HTML, CSS or JavaScript required.

Check it out


Collective 690 item image

cccolor

A clean, simple & elegant color picker, with color values automagically available as RGB, HSL, hex, or 8-digit hex (with alpha). Perfect for CSS and HTML.

Check it out


Collective 690 item image

There Is No Digital World

A great read by Christopher Butler: Everything digital costs something physical. It’s time for a digital conservation movement.

Read it










Collective 690 item image

Throos

Throos makes product walkthroughs as easy as can be. It allows you to add highly engaging & customizable walkthroughs to your software product — in minutes.

Check it out




Collective 690 item image

Erba matta

Magic grass made by Fabio Ottaviani using InstancedMesh, custom attributes and shaderMaterial in Three.js.

Check it out



Collective 690 item image

nnnoise

An SVG generator to create subtle noise textures made by Sébastien Noël.

Check it out


The post Collective #690 appeared first on Codrops.

Some Articles About Accessibility I’ve Saved Recently IV


The post Some Articles About Accessibility I’ve Saved Recently IV appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Nailing the Perfect Contrast Between Light Text and a Background Image

Have you ever come across a site where light text is sitting on a light background image? If you have, you’ll know how difficult that is to read. A popular way to avoid that is to use a transparent overlay. But this leads to an important question: Just how transparent should that overlay be? It’s not like we’re always dealing with the same font sizes, weights, and colors, and, of course, different images will result in different contrasts.

Trying to stamp out poor text contrast on background images is a lot like playing Whac-a-Mole. Instead of guessing, we can solve this problem with HTML <canvas> and a little bit of math.

Like this:

We could say “Problem solved!” and simply end this article here. But where’s the fun in that? What I want to show you is how this tool works so you have a new way to handle this all-too-common problem.

Here’s the plan

First, let’s get specific about our goals. We’ve said we want readable text on top of a background image, but what does “readable” even mean? For our purposes, we’ll use the WCAG definition of AA-level readability, which says text and background colors need enough contrast between them such that that one color is 4.5 times lighter than the other.

Let’s pick a text color, a background image, and an overlay color as a starting point. Given those inputs, we want to find the overlay opacity level that makes the text readable without hiding the image so much that it, too, is difficult to see. To complicate things a bit, we’ll use an image with both dark and light space and make sure the overlay takes that into account.

Our final result will be a value we can apply to the CSS opacity property of the overlay that gives us the right amount of transparency that makes the text 4.5 times lighter than the background.

Optimal overlay opacity: 0.521

To find the optimal overlay opacity we’ll go through four steps:

  1. We’ll put the image in an HTML <canvas>, which will let us read the colors of each pixel in the image.
  2. We’ll find the pixel in the image that has the least contrast with the text.
  3. Next, we’ll prepare a color-mixing formula we can use to test different opacity levels on top of that pixel’s color.
  4. Finally, we’ll adjust the opacity of our overlay until the text contrast hits the readability goal. And these won’t just be random guesses — we’ll use binary search techniques to make this process quick.

Let’s get started!

Step 1: Read image colors from the canvas

Canvas lets us “read” the colors contained in an image. To do that, we need to “draw” the image onto a <canvas> element and then use the canvas context (ctx) getImageData() method to produce a list of the image’s colors.

function getImagePixelColorsUsingCanvas(image, canvas) {
  // The canvas's context (often abbreviated as ctx) is an object
  // that contains a bunch of functions to control your canvas
  const ctx = canvas.getContext('2d');


  // The width can be anything, so I picked 500 because it's large
  // enough to catch details but small enough to keep the
  // calculations quick.
  canvas.width = 500;


  // Make sure the canvas matches proportions of our image
  canvas.height = (image.height / image.width) * canvas.width;


  // Grab the image and canvas measurements so we can use them in the next step
  const sourceImageCoordinates = [0, 0, image.width, image.height];
  const destinationCanvasCoordinates = [0, 0, canvas.width, canvas.height];


  // Canvas's drawImage() works by mapping our image's measurements onto
  // the canvas where we want to draw it
  ctx.drawImage(
    image,
    ...sourceImageCoordinates,
    ...destinationCanvasCoordinates
  );


  // Remember that getImageData only works for same-origin or 
  // cross-origin-enabled images.
  // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
  const imagePixelColors = ctx.getImageData(...destinationCanvasCoordinates);
  return imagePixelColors;
}

The getImageData() method gives us a list of numbers representing the colors in each pixel. Each pixel is represented by four numbers: red, green, blue, and opacity (also called “alpha”). Knowing this, we can loop through the list of pixels and find whatever info we need. This will be useful in the next step.

Image of a blue and purple rose on a light pink background. A section of the rose is magnified to reveal the RGBA values of a specific pixel.

Step 2: Find the pixel with the least contrast

Before we do this, we need to know how to calculate contrast. We’ll write a function called getContrast() that takes in two colors and spits out a number representing the level of contrast between the two. The higher the number, the better the contrast for legibility.

When I started researching colors for this project, I was expecting to find a simple formula. It turned out there were multiple steps.

To calculate the contrast between two colors, we need to know their luminance levels, which is essentially the brightness (Stacie Arellano does a deep dive on luminance that’s worth checking out.)

Thanks to the W3C, we know the formula for calculating contrast using luminance:

const contrast = (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05);

Getting the luminance of a color means we have to convert the color from the regular 8-bit RGB value used on the web (where each color is 0-255) to what’s called linear RGB. The reason we need to do this is that brightness doesn’t increase evenly as colors change. We need to convert our colors into a format where the brightness does vary evenly with color changes. That allows us to properly calculate luminance. Again, the W3C is a help here:

const luminance = (0.2126 * getLinearRGB(r) + 0.7152 * getLinearRGB(g) + 0.0722 * getLinearRGB(b));

But wait, there’s more! In order to convert 8-bit RGB (0 to 255) to linear RGB, we need to go through what’s called standard RGB (also called sRGB), which is on a scale from 0 to 1.

So the process goes: 

8-bit RGB → standard RGB  → linear RGB → luminance

And once we have the luminance of both colors we want to compare, we can plug in the luminance values to get the contrast between their respective colors.

// getContrast is the only function we need to interact with directly.
// The rest of the functions are intermediate helper steps.
function getContrast(color1, color2) {
  const color1_luminance = getLuminance(color1);
  const color2_luminance = getLuminance(color2);
  const lighterColorLuminance = Math.max(color1_luminance, color2_luminance);
  const darkerColorLuminance = Math.min(color1_luminance, color2_luminance);
  const contrast = (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05);
  return contrast;
}


function getLuminance({r,g,b}) {
  return (0.2126 * getLinearRGB(r) + 0.7152 * getLinearRGB(g) + 0.0722 * getLinearRGB(b));
}
function getLinearRGB(primaryColor_8bit) {
  // First convert from 8-bit rbg (0-255) to standard RGB (0-1)
  const primaryColor_sRGB = convert_8bit_RGB_to_standard_RGB(primaryColor_8bit);


  // Then convert from sRGB to linear RGB so we can use it to calculate luminance
  const primaryColor_RGB_linear = convert_standard_RGB_to_linear_RGB(primaryColor_sRGB);
  return primaryColor_RGB_linear;
}
function convert_8bit_RGB_to_standard_RGB(primaryColor_8bit) {
  return primaryColor_8bit / 255;
}
function convert_standard_RGB_to_linear_RGB(primaryColor_sRGB) {
  const primaryColor_linear = primaryColor_sRGB < 0.03928 ?
    primaryColor_sRGB/12.92 :
    Math.pow((primaryColor_sRGB + 0.055) / 1.055, 2.4);
  return primaryColor_linear;
}

Now that we can calculate contrast, we’ll need to look at our image from the previous step and loop through each pixel, comparing the contrast between that pixel’s color and the foreground text color. As we loop through the image’s pixels, we’ll keep track of the worst (lowest) contrast so far, and when we reach the end of the loop, we’ll know the worst-contrast color in the image.

function getWorstContrastColorInImage(textColor, imagePixelColors) {
  let worstContrastColorInImage;
  let worstContrast = Infinity; // This guarantees we won't start too low
  for (let i = 0; i < imagePixelColors.data.length; i += 4) {
    let pixelColor = {
      r: imagePixelColors.data[i],
      g: imagePixelColors.data[i + 1],
      b: imagePixelColors.data[i + 2],
    };
    let contrast = getContrast(textColor, pixelColor);
    if(contrast < worstContrast) {
      worstContrast = contrast;
      worstContrastColorInImage = pixelColor;
    }
  }
  return worstContrastColorInImage;
}

Step 3: Prepare a color-mixing formula to test overlay opacity levels

Now that we know the worst-contrast color in our image, the next step is to establish how transparent the overlay should be and see how that changes the contrast with the text.

When I first implemented this, I used a separate canvas to mix colors and read the results. However, thanks to Ana Tudor’s article about transparency, I now know there’s a convenient formula to calculate the resulting color from mixing a base color with a transparent overlay.

For each color channel (red, green, and blue), we’d apply this formula to get the mixed color:

mixedColor = baseColor + (overlayColor - baseColor) * overlayOpacity

So, in code, that would look like this:

function mixColors(baseColor, overlayColor, overlayOpacity) {
  const mixedColor = {
    r: baseColor.r + (overlayColor.r - baseColor.r) * overlayOpacity,
    g: baseColor.g + (overlayColor.g - baseColor.g) * overlayOpacity,
    b: baseColor.b + (overlayColor.b - baseColor.b) * overlayOpacity,
  }
  return mixedColor;
}

Now that we’re able to mix colors, we can test the contrast when the overlay opacity value is applied.

function getTextContrastWithImagePlusOverlay({textColor, overlayColor, imagePixelColor, overlayOpacity}) {
  const colorOfImagePixelPlusOverlay = mixColors(imagePixelColor, overlayColor, overlayOpacity);
  const contrast = getContrast(this.state.textColor, colorOfImagePixelPlusOverlay);
  return contrast;
}

With that, we have all the tools we need to find the optimal overlay opacity!

Step 4: Find the overlay opacity that hits our contrast goal

We can test an overlay’s opacity and see how that affects the contrast between the text and image. We’re going to try a bunch of different opacity levels until we find the contrast that hits our mark where the text is 4.5 times lighter than the background. That may sound crazy, but don’t worry; we’re not going to guess randomly. We’ll use a binary search, which is a process that lets us quickly narrow down the possible set of answers until we get a precise result.

Here’s how a binary search works:

  • Guess in the middle.
  • If the guess is too high, we eliminate the top half of the answers. Too low? We eliminate the bottom half instead.
  • Guess in the middle of that new range.
  • Repeat this process until we get a value.

I just so happen to have a tool to show how this works:

In this case, we’re trying to guess an opacity value that’s between 0 and 1. So, we’ll guess in the middle, test whether the resulting contrast is too high or too low, eliminate half the options, and guess again. If we limit the binary search to eight guesses, we’ll get a precise answer in a snap.

Before we start searching, we’ll need a way to check if an overlay is even necessary in the first place. There’s no point optimizing an overlay we don’t even need!

function isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast) {
  const contrastWithoutOverlay = getContrast(textColor, worstContrastColorInImage);
  return contrastWithoutOverlay < desiredContrast;
}

Now we can use our binary search to look for the optimal overlay opacity:

function findOptimalOverlayOpacity(textColor, overlayColor, worstContrastColorInImage, desiredContrast) {
  // If the contrast is already fine, we don't need the overlay,
  // so we can skip the rest.
  const isOverlayNecessary = isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast);
  if (!isOverlayNecessary) {
    return 0;
  }


  const opacityGuessRange = {
    lowerBound: 0,
    midpoint: 0.5,
    upperBound: 1,
  };
  let numberOfGuesses = 0;
  const maxGuesses = 8;


  // If there's no solution, the opacity guesses will approach 1,
  // so we can hold onto this as an upper limit to check for the no-solution case.
  const opacityLimit = 0.99;


  // This loop repeatedly narrows down our guesses until we get a result
  while (numberOfGuesses < maxGuesses) {
    numberOfGuesses++;


    const currentGuess = opacityGuessRange.midpoint;
    const contrastOfGuess = getTextContrastWithImagePlusOverlay({
      textColor,
      overlayColor,
      imagePixelColor: worstContrastColorInImage,
      overlayOpacity: currentGuess,
    });


    const isGuessTooLow = contrastOfGuess < desiredContrast;
    const isGuessTooHigh = contrastOfGuess > desiredContrast;
    if (isGuessTooLow) {
      opacityGuessRange.lowerBound = currentGuess;
    }
    else if (isGuessTooHigh) {
      opacityGuessRange.upperBound = currentGuess;
    }


    const newMidpoint = ((opacityGuessRange.upperBound - opacityGuessRange.lowerBound) / 2) + opacityGuessRange.lowerBound;
    opacityGuessRange.midpoint = newMidpoint;
  }


  const optimalOpacity = opacityGuessRange.midpoint;
  const hasNoSolution = optimalOpacity > opacityLimit;


  if (hasNoSolution) {
    console.log('No solution'); // Handle the no-solution case however you'd like
    return opacityLimit;
  }
  return optimalOpacity;
}

With our experiment complete, we now know exactly how transparent our overlay needs to be to keep our text readable without hiding the background image too much.

We did it!

Improvements and limitations

The methods we’ve covered only work if the text color and the overlay color have enough contrast to begin with. For example, if you were to choose a text color that’s the same as your overlay, there won’t be an optimal solution unless the image doesn’t need an overlay at all.

In addition, even if the contrast is mathematically acceptable, that doesn’t always guarantee it’ll look great. This is especially true for dark text with a light overlay and a busy background image. Various parts of the image may distract from the text, making it difficult to read even when the contrast is numerically fine. That’s why the popular recommendation is to use light text on a dark background.

We also haven’t taken where the pixels are located into account or how many there are of each color. One drawback of that is that a pixel in the corner could possibly exert too much influence on the result. The benefit, however, is that we don’t have to worry about how the image’s colors are distributed or where the text is because, as long as we’ve handled where the least amount of contrast is, we’re safe everywhere else.

I learned a few things along the way

There are some things I walked away with after this experiment, and I’d like to share them with you:

  • Getting specific about a goal really helps! We started with a vague goal of wanting readable text on an image, and we ended up with a specific contrast level we could strive for.
  • It’s so important to be clear about the terms. For example, standard RGB wasn’t what I expected. I learned that what I thought of as “regular” RGB (0 to 255) is formally called 8-bit RGB. Also, I thought the “L” in the equations I researched meant “lightness,” but it actually means “luminance,” which is not to be confused with “luminosity.” Clearing up terms helps how we code as well as how we discuss the end result.
  • Complex doesn’t mean unsolvable. Problems that sound hard can be broken into smaller, more manageable pieces.
  • When you walk the path, you spot the shortcuts. For the common case of white text on a black transparent overlay, you’ll never need an opacity over 0.54 to achieve WCAG AA-level readability.

In summary…

You now have a way to make your text readable on a background image without sacrificing too much of the image. If you’ve gotten this far, I hope I’ve been able to give you a general idea of how it all works.

I originally started this project because I saw (and made) too many website banners where the text was tough to read against a background image or the background image was overly obscured by the overlay. I wanted to do something about it, and I wanted to give others a way to do the same. I wrote this article in hopes that you’d come away with a better understanding of readability on the web. I hope you’ve learned some neat canvas tricks too.

If you’ve done something interesting with readability or canvas, I’d love to hear about it in the comments!


The post Nailing the Perfect Contrast Between Light Text and a Background Image appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Adventures in CSS Semi-Transparency Land

Recently, I was asked to make some tweaks to a landing page and, among the things I found in the code, there were two semitransparent overlays — both with the same RGB values for the background-color — on top of an image. Something like this:

<img src='myImage.jpg'/>
<div class='over1'></div>
<div class='over2'></div>

There was no purpose to having two of them other than the fact that having just one didn't tint the image enough. For some reason, whoever initially coded that page thought that adding another semitransparent overlay was a better idea than increasing the opacity of the first.

So, I decided to ditch a layer and give the remaining one an opacity value that would give a visual result equivalent to the initial one, given by using two layers. Alright, but how do we get the opacity of the one layer equivalent?

If you remember my crash course in mask compositing, then you may have guessed the answer because it's the exact same formula that we also use for the add compositing operation! Given two layers with alphas a0 and a1, the resulting alpha is:

a0 + a1 - a0⋅a1

The interactive demo below shows a comparative look at a two-layer overlay with alphas a0 and a1 (which you can control via the range inputs) versus a one layer overlay with an alpha of a0 + a1 - a0⋅a1.

See the Pen by thebabydino (@thebabydino) on CodePen.

Funny enough, they look identical if we remove the image (via the checkbox at the bottom of the demo), but seem a bit different with the image underneath. Perhaps the difference is just my eyes playing tricks on me given the image is lighter in some parts and darker in others.

It definitely doesn't look different if we don't have them side by side and we just switch between the two layers of alphas a0 and a1 and the one layer of alpha a0 + a1 - a0⋅a1.

See the Pen by thebabydino (@thebabydino) on CodePen.

This can be extended to multiple layers. In this case, we compute the equivalent layer of the bottom two layers, then the equivalent layer of this result and the layer right on top of it, and so on:

Diagram. Illustrates how a bunch of semitransparent layers of various alphas are reduced to a single one. We start by taking the first two from the bottom and computing their equivalent, then we take this result and the third layer from the bottom and combine them into a single layer and so on.
Reducing multiple semitransparent layers to a single one.

Playing with this has also made me wonder about the solid background equivalent of a solid layer (c0) with a semitransparent overlay (c1 with an alpha of a) on top. In this case, the background of the one layer equivalent is computed on a per channel basis, with the resulting channels being:

ch0 + (ch1 - ch0)*a

...where ch0 is a channel (red, green or blue) of the solid bottom layer, ch1 the corresponding channel of the top semitransparent layer, and a the alpha of the same top semitransparent layer.

Putting this into Sass code, we have:

/* per channel function */
@function res-ch($ch0, $ch1, $a) {
  @return $ch0 + ($ch1 - $ch0)*$a
}

@function res-col($c0, $c1, $a) {
  $ch: 'red' 'green' 'blue'; /* channel names */
  $nc: length($ch); /* number of channels */
  $ch-list: ();

  @for $i from 0 to $nc {
    $fn: nth($ch, $i + 1);
    $ch-list: $ch-list, 
      res-ch(call($fn, $c0), call($fn, $c1), $a);
  }

  @return RGB($ch-list)
}

The interactive demo below (which lets us pick the RGB values of the two layers as well as the alpha of the top one by clicking the swatches and the alpha value, respectively) shows a comparative look at the two layer versus our computed one layer equivalent.

See the Pen by thebabydino (@thebabydino) on CodePen.

Depending on the device, operating system and browser, you may see the two panels in the demo above have identical backgrounds... or not. The formula is correct, but how different browsers on different operating systems and devices deal with the two layer case may vary.

Screenshot collage.
Expected result with panels being identical on the left vs. the slightly different result we may sometimes get between the two layer scenario (top right) and the one layer scenario (bottom right).

I asked for screenshots of a simplified test case on Twitter and, from the replies that I got, the two panels always seem to look the same on mobile browsers, regardless of whether we're talking about Android or iOS devices as well as Firefox, regardless of the operating system. They also seem to almost always be identical on Windows, though I did receive a reply letting me know both Chrome and Chromium Edge may sometimes show the two panels differently.

When it comes to WebKit browsers on macOS and Linux, results are very much mixed, with the panels slightly different in most cases. That said, switching to a sRGB profile could make them identical. The funniest thing here is, when using a two monitor setup, dragging the window from one monitor to the other can make the difference whether the two panels appear or disappear.

However, in a real use case scenario, the difference is pretty small and we're never going to have the two panels side-by-side. Even if there's a difference, nobody is going to know about it unless they test the page in different scenarios, which is something probably only a web developer would do anyway. And it's not like we don't also have differences between how the same plain old solid backgrounds look on different devices, operating systems and browsers. For example, #ffa800, which gets used a lot here on CSS-Tricks, doesn't look the same on my Ubuntu and Windows laptops. The same can be said about the way people's eyes may perceive things differently.

The post Adventures in CSS Semi-Transparency Land appeared first on CSS-Tricks.

Understanding Web Accessibility Color Contrast Guidelines and Ratios

What should you do when you get a complaint about the color contrast in your web design? It might seem perfectly fine to you because you’re able to read content throughout the site, but to someone else, it might be a totally different experience. How can put yourself in that person’s shoes to improve their experience?

There are some relatively easy ways to test contrast. For example, you can check the site on your phone or tablet in bright sunlight, or add a CSS filter to mimic a grayscale view). But… you don’t have to trust your eyes. Not everyone has your exact eyes anyway, so your subjective opinion can possibly be a faulty measurement. 

You can mathematically know if two colors have enough contrast between them. 

The W3C has a document called Web Content Accessibility Guidelines (WCAG) 2.1 that covers  successful contrast guidelines. Before we get to the math, we need to know what contrast ratio scores we are aiming to meet or exceed. To get a passing grade (AA), the contrast ratio is 4.5:1 for most body text and 3:1 for larger text. 

How did the W3C arrive at these ratios?

The guidelines were created for anyone using a standard browser, with no additional assistive technology. The contrast ratios that the WCAG suggests were based initially on earlier contrast standards and adjusted to accommodate newer display technologies, like antialiased text, so content would be readable by people with a variety of visual or cognitive difficulties, whether it be due to age, sickness, or other losses of visual acuity.  

We’re basically aiming to make text readable for someone with 20/40 vision, which is equivilent to the vision of someone 80 years old. Visual acuity of 20/40 means you can only read something at 20 feet away that someone with perfect 20/20 vision could read if it was 40 feet away.

So, say your design calls for antialiased text because it looks much smoother on a screen. It actually sacrifices a bit of contrast and ding your ratio. The WCAG goes into more detail on how scoring works.

There are other standards that take contrast in consideration, and the WCAG used some of these considerations to develop their scoring. One is called the Human Factors Engineering of Computer Workstations (ANSI/HFES 100-2007) was published in 2007 and designated as an American standard for ergonomics. It combined and replaced two earlier standards that were created by separate committees. The goal of the combined standard was to accommodate 90% of computer users, and cover many aspects of computer use and ergonomics, including visual displays and contrast. So, that means we have physical screens to consider in our designs.

What does the ratio mean?

The contrast ratio explains the difference between the lightest color brightness and the darkest color brightness in a given range. It’s the relative luminance of each color.

Let’s start with an egregious example of a teal color text on a light gray background. 

<h1>Title of Your Awesome Site</h1>
h1 {
  background-color: #1ABC9C;
  color: #888888;
}
Yikes!

It’s worth calling out that some tools, like WordPress, provide a helpful warning for this when there’s a poorly contrasted text and background combination. In the case of WordPress, a you get notice in the sidebar.

"This color combination may be hard for people to read. Try using a brighter background color and/or a darker text color."

“OK,” you say. “Perhaps you think that teal on gray color combination is not exactly great, but I can still make out what the content says.“ (I’m glad one of us can because it’s pretty much a muddy gray mess to me.)

The contrast ratio for that fine piece of hypertext is 1.47:1.

I wanted a better understanding of what the contrast scores were actually checking and came to find that it requires the use of mathematics… with a side of understanding the differences between human and computer vision.  This journey taught me about the history of computer vision and a bit about biology, and gave me a small review of some math concepts I haven’t touched since college.

Here’s the equation:

(L1 + 0.05) / (L2 + 0.05)
  • L1 is the relative luminance of the lighter of the colors.
  • L2 is the relative luminance of the darker of the colors.

This seems simple, right? But first we need to determine the relative luminance for each color to get those variables.

OK, back to relative luminance

We mentioned it in passing, but it’s worth going deeper into relative luminance, or the relative brightness of any color expressed into a spectrum between 0 (black) and 1 (white).

To determine the relative luminance for each color, we first need to get the RGB notation for a color. Sometimes we’re working with HEX color values and need to covert that over to RGB. There are online calculators that will do this for us, but there’s solid math happening in the background that makes it happen. Our teal hex color, #1ABC9C, becomes an RGB of 26, 188, 156.

Next, we take each value of the RGB color and divide each one by 255 (the max integer of RGB values) to get a linear value between 0 and 1. 

So now with our teal color it looks like this:

ComponentEquationValue
Red26/2550.10196078
Green188/2550.73725490
Blue156/2550.61176471

Then we apply gamma correction, which defines the relationship between a pixel's numerical value and its actual luminance, to each component part of the RGB color. If the linear value of a component is less than .03938, we divide it by 12.92. Otherwise, we add .055 and divide the total by 1.055 and take the result to the power of 2.4.

Our gamma corrected color components from our teal color end up like this:

ComponentEquationValue
Red((0.10196078 +.055)/1.055) ^ 2.40.01032982
Green((0.73725490 +.055)/1.055) ^ 2.40.50288646
Blue((0.61176471 +.055)/1.055) ^ 2.40.33245154

This part of our equation comes from the formula for determining relative luminance.

We just sort of sped past gamma correction there without talking much about it and what it does. In short, it translates what a computer "sees” into the human perception of brightness. Computers record light directly where twice the photons equals twice the brightness. Human eyes perceive more levels of light in dim conditions and fewer in bright conditions. The digital devices around us make gamma encoding and decoding calculations all the time. It’s used to show us things on the screens that match up to our perception of how things appear to our eyes.

Finally, we multiply the different colors by numbers that signify how bright that color appears to the human eye. That means we determine the luminance of each color by multiplying the red component value by .2126, the green component value by .7152, and the blue component by .0722 before adding all three of those results together. You'll note that green gets the highest value here,

So, one last time for teal:

ComponentEquationValue
Red0.01032982  X 0.21260.00219611973
Green0.50288646  X 0.71520.35966439619
Blue0.33245154  X 0.07220.02400300118

...and add them together for luminance!

L1 = 0.00219611973 + 0.35966439619 + 0.02400300118 = 0.38586352

If we do the same to get our L2 value, that gives us 0.24620133.

We finally have the L1 and L2 values we need to calculate contrast. To determine which value is  L1 and and which is L2 , we need to make sure that the larger number (which shows the lighter color) is always L1 and is divided by the smaller/darker color as L2.

Now compare that result with the WCAG success criterias. For standard text size, between 18-22 points, a minimul result of 4.5 will pass with a grade of AA. If our text is larger, then a slightly lower score of  3 will do the job. But to get the highest WCAG grade (AAA), we have to have a contrast ratio result of at least 7. Our lovely combination fails all tests, coming far under 4.5 for regular text or 3 for headline style text. Time to choose some better colors!

I’m so glad we have computers and online tools to do this work for us! Trying to work out the details step-by-step on paper gave me a couple weeks of frustration. It was a lot of me getting things wrong when comparing results to those of automated contrast checkers.

Remember how teachers in school always wanted you to show your math work to prove how you got to the answer? I made something to help us out.

If you view this demo with the console open, you’ll see the math that goes into each step of the calculations. Go ahead, try our two example colors, like #1ABC9C and #888888.

I just want my page to have proper contrast, what do I do?!

There are a variety of accessibility resources that you can can audit your site. Here’s a list I put together, and there’s another list here on CSS-Tricks.

But here are a few tips to get you started.

First, identify areas that are not serving your accessibility needs.

The WAVE accessibility tool is a good place to start. Run your site through that and it will give you contrast results and help identify trouble areas.

Yay, passing scores!

Follow the suggestions of the audit

Use best practices to improve your scores, and remove the errors. Once you identify contrast errors, you can try out some different options right there in the WAVE tool. Click on the color box to pop open a color picker. Then play around until the errors go away, and you’ll know what you can replace in your code.

Run the test again

This way, you can make sure your changes improved things. Congratulations! You just made your product better for all users, not just ones affected by the accessibility errors!

What comes next is up to you!

You can make it easier on yourself and start all new products with the goal of making them accessible. Make accessibility guidelines part of your requirements for both technology and design. You’ll save yourself potentially hundreds of hours of remediation, and potential legal complaints. U.S. government and education websites are required to comply, but other industries are often taken to task for not making their sites equally available for all people.

If you have the option, consider using established and tested frameworks and web libraries (like Bootstrap or Google’s Material Design) that have already figured out optimum contrast theme colors. In many cases, you can take just what you need (like only the CSS) or at least review their color palettes to inform choices. You should still check the contrast though because, while most standard text options in a framework may follow contrast ratio WCAG suggestions, things like alert and message styles may not. (I’m looking at you, Bootstrap!)

Derek Kay has reviewed a list of web frameworks with a focus on accessibility, which I suggest you read if you are looking for more options. The U.S. Web Design System shows one way to solve color/contrast puzzles using their CSS token system that labels colors to make contrast differences super clear), but they also link to several very good resources for improving and understanding contrast.

We took a deeper dive here than perhaps you ever really need to know, but understanding what a contrast ratio is and what it actually means should help you remember to keep contrast in mind when designing future sites, web apps, and other software.

Having a clearer understanding of what the contrast ratio means helps me to remember who poor contrast can affect, and how to improve web and mobile products overall.

I’m not the ultimate subject expert on contrast, just a very, very curious girl who sometimes has issues reading things on the web with low contrast.

If you have any additional thoughts, corrections or further research to share, please leave a comment and I’ll amend this article! The fuller our understanding of the needs and requirements of our sites is, the better we can plan improvements and ultimately serve the needs of our audiences.

The post Understanding Web Accessibility Color Contrast Guidelines and Ratios appeared first on CSS-Tricks.

CSS Variables + calc() + rgb() = Enforcing High Contrast Colors

As you may know, the recent updates and additions to CSS are extremely powerful. From Flexbox to Grid, and — what we’re concerned about here — Custom Properties (aka CSS variables), all of which make robust and dynamic layouts and interfaces easier than ever while opening up many other possibilities we used to only dream of.

The other day, I was thinking that there must be a way to use Custom Properties to color an element's background while maintaining a contrast with the foreground color that is high enough (using either white or black) to pass WCAG AA accessibility standards.

It’s astonishingly efficient to do this in JavaScript with a few lines of code:

var rgb = [255, 0, 0];

function setForegroundColor() {
  var sum = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) / 1000);
  return (sum > 128) ? 'black' : 'white';
}

This takes the red, green and blue (RGB) values of an element’s background color, multiplies them by some special numbers (299, 587, and 144, respectively), adds them together, then divides the total by 1,000. When that sum is greater than 128, it will return black; otherwise, we’ll get white. Not too bad.

The only problem is, when it comes to recreating this in CSS, we don't have access to a native if statement to evaluate the sum. So,how can we replicate this in CSS without one?

Luckily, like HTML, CSS can be very forgiving. If we pass a value greater than 255 into the RGB function, it will get capped at 255. Same goes for numbers lower than 0. Even negative integers will get capped at 0. So, instead of testing whether our sum is greater or less than 128, we subtract 128 from our sum, giving us either a positive or negative integer. Then, if we multiply it by a large negative value (e.g. -1,000), we end up with either very large positive or negative values that we can then pass into the RGB function. Like I said earlier, this will get capped to the browser’s desired values.

Here is an example using CSS variables:

:root {
  --red: 28;
  --green: 150;
  --blue: 130;

  --accessible-color: calc(
    (
      (
        (var(--red) * 299) +
        (var(--green) * 587) +
        (var(--blue) * 114) /
        1000
      ) - 128
    ) * -1000
  );
}

.button {
  color:
    rgb(
      var(--accessible-color),
      var(--accessible-color),
      var(--accessible-color)
    );
  background-color:
    rgb(
      var(--red),
      var(--green),
      var(--blue)
    );
}

If my math is correct (and it's very possible that it's not) we get a total of 16,758, which is much greater than 255. Pass this total into the rgb() function for all three values, and the browser will set the text color to white.

At this point, everything seems to be working in both Chrome and Firefox, but Safari is a little cranky and gives a different result. At first, I thought this might be because Safari was not capping the large values I was providing in the function, but after some testing, I found that Safari didn't like the division in my calculation for some reason.

Taking a closer look at the calc() function, I noticed that I could remove the division of 1,000 by increasing the value of 128 to 128,000. Here’s how that looks so far:

:root {
  --red: 28;
  --green: 150;
  --blue: 130;

  --accessible-color: calc(
    (
      (
        (var(--red) * 299) +
        (var(--green) * 587) +
        (var(--blue) * 114)
      ) - 128000 /* HIGHLIGHT */
    ) * -1000
  );
}

.button {
  color:
    rgb(
      var(--accessible-color),
      var(--accessible-color),
      var(--accessible-color)
    );
  background-color:
    rgb(
      var(--red),
      var(--green),
      var(--blue)
    );
}

Throw in a few range sliders to adjust the color values, and there you have it: a dynamic UI element that can swap text color based on its background-color while maintaining a passing grade with WCAG AA.

See the Pen
CSS Only Accessible Button
by Josh Bader (@joshbader)
on CodePen.

Putting this concept to practical use

Below is a Pen showing how this technique can be used to theme a user interface. I have duplicated and moved the --accessible-color variable into the specific CSS rules that require it, and to help ensure backgrounds remain accessible based on their foregrounds, I have multiplied the --accessible-color variable by -1 in several places. The colors can be changed by using the controls located at the bottom-right. Click the cog/gear icon to access them.

See the Pen
CSS Variable Accessible UI
by Josh Bader (@joshbader)
on CodePen.

There are other ways to do this

A little while back, Facundo Corradini explained how to do something very similar in this post. He uses a slightly different calculation in combination with the hsl function. He also goes into detail about some of the issues he was having while coming up with the concept:

Some hues get really problematic (particularly yellows and cyans), as they are displayed way brighter than others (e.g. reds and blues) despite having the same lightness value. In consequence, some colors are treated as dark and given white text despite being extremely bright.

What in the name of CSS is going on?

He goes on to mention that Edge wasn’t capping his large numbers, and during my testing, I noticed that sometimes it was working and other times it was not. If anyone can pinpoint why this might be, feel free to share in the comments.

Further, Ana Tudor explains how using filter + mix-blend-mode can help contrast text against more complex backgrounds. And, when I say complex, I mean complex. She even goes so far as to demonstrate how text color can change as pieces of the background color change — pretty awesome!

Also, Robin Rendle explains how to use mix-blend-mode along with pseudo elements to automatically reverse text colors based on their background-color.

So, count this as yet another approach to throw into the mix. It’s incredibly awesome that Custom Properties open up these sorts of possibilities for us while allowing us to solve the same problem in a variety of ways.

The post CSS Variables + calc() + rgb() = Enforcing High Contrast Colors appeared first on CSS-Tricks.