Tag: Properties

How I Made an Icon System Out of CSS Custom Properties

SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover and you can even animate the icons with CSS or JavaScript.

There are many ways to include an SVG on a page and each technique has its own advantages and disadvantages. For the last couple of years, I have been using a Sass function to import directly my icons in my CSS and avoid having to mess up my HTML markup.

I have a Sass list with all the source codes of my icons. Each icon is then encoded into a data URI with a Sass function and stored in a custom property on the root of the page.


What I have for you here is a Sass function that creates a SVG icon library directly in your CSS.

The SVG source code is compiled with the Sass function that encodes them in data URI and then stores the icons in CSS custom properties. You can then use any icon anywhere in your CSS like as if it was an external image.

This is an example pulled straight from the code of my personal site:

.c-filters__summary h2:after {   content: var(--svg-down-arrow);   position: relative;   top: 2px;   margin-left: auto;   animation: closeSummary .25s ease-out; }


Sass structure

/* All the icons source codes */ $  svg-icons: (   burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...' );  /* Sass function to encode the icons */ @function svg($  name) {   @return url('data:image/svg+xml, #{$  encodedSVG} '); }  /* Store each icon into a custom property */ :root {   @each $  name, $  code in $  svg-icons {     --svg-#{$  name}: #{svg($  name)};   } }  /* Append a burger icon in my button */ .menu::after {   content: var(--svg-burger); }		

This technique has both pros and cons, so please take them into account before implementing this solution on your project:


  • There are no HTTP requests for the SVG files.
  • All of the icons are stored in one place.
  • If you need to update an icon, you don’t have to go over each HTML templates file.
  • The icons are cached along with your CSS.
  • You can manually edit the source code of the icons.
  • It does not pollute your HTML by adding extra markup.
  • You can still change the color or some aspect of the icon with CSS.


  • You cannot animate or update a specific part of the SVG with CSS.
  • The more icons you have, the heavier your CSS compiled file will be.

I mostly use this technique for icons rather than logos or illustrations. An encoded SVG is always going to be heavier than its original file, so I still load my complex SVG with an external file either with an <img> tag or in my CSS with url(path/to/file.svg).

Encoding SVG into data URI

Encoding your SVG as data URIs is not new. In fact Chris Coyier wrote a post about it over 10 years ago to explain how to use this technique and why you should (or should not) use it.

There are two ways to use an SVG in your CSS with data URI:

  • As an external image (using background-image,border-image,list-style-image,…)
  • As the content of a pseudo element (e.g. ::before or ::after)

Here is a basic example showing how you how to use those two methods:

The main issue with this particular implementation is that you have to convert the SVG manually every time you need a new icon and it is not really pleasant to have this long string of unreadable code in your CSS.

This is where Sass comes to the rescue!

Using a Sass function

By using Sass, we can make our life simpler by copying the source code of our SVG directly in our codebase, letting Sass encode them properly to avoid any browser error.

This solution is mostly inspired by an existing function developed by Threespot Media and available in their repository.

Here are the four steps of this technique:

  • Create a variable with all your SVG icons listed.
  • List all the characters that needs to be skipped for a data URI.
  • Implement a function to encode the SVGs to a data URI format.
  • Use your function in your code.

1. Icons list

/** * Add all the icons of your project in this Sass list */ $  svg-icons: (   burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>' );

2. List of escaped characters

/** * Characters to escape from SVGs * This list allows you to have inline CSS in your SVG code as well */ $  fs-escape-chars: (   ' ': '%20',   '\'': '%22',   '"': '%27',   '#': '%23',   '/': '%2F',   ':': '%3A',   '(': '%28',   ')': '%29',   '%': '%25',   '<': '%3C',   '>': '%3E',   '\': '%5C',   '^': '%5E',   '{': '%7B',   '|': '%7C',   '}': '%7D', );

3. Encode function

/** * You can call this function by using `svg(nameOfTheSVG)` */ @function svg($  name) {   // Check if icon exists   @if not map-has-key($  svg-icons, $  name) {     @error 'icon “#{$  name}” does not exists in $  svg-icons map';     @return false;   }    // Get icon data   $  icon-map: map-get($  svg-icons, $  name);    $  escaped-string: '';   $  unquote-icon: unquote($  icon-map);   // Loop through each character in string   @for $  i from 1 through str-length($  unquote-icon) {     $  char: str-slice($  unquote-icon, $  i, $  i);      // Check if character is in symbol map     $  char-lookup: map-get($  fs-escape-chars, $  char);      // If it is, use escaped version     @if $  char-lookup != null {         $  char: $  char-lookup;     }      // Append character to escaped string     $  escaped-string: $  escaped-string + $  char;   }    // Return inline SVG data   @return url('data:image/svg+xml, #{$  escaped-string} '); }		

4. Add an SVG in your page

button {   &::after {     /* Import inline SVG */     content: svg(burger);   } }

If you have followed those steps, Sass should compile your code properly and output the following:

button::after {   content: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2024.8%2018.92%27%20width=%2724.8%27%20height=%2718.92%27%3E%3Cpath%20d=%27M23.8,9.46H1m22.8,8.46H1M23.8,1H1%27%20fill=%27none%27%20stroke=%27%23000%27%20stroke-linecap=%27round%27%20stroke-width=%272%27%2F%3E%3C%2Fsvg%3E "); }		

Custom properties

The now-implemented Sass svg() function works great. But its biggest flaw is that an icon that is needed in multiple places in your code will be duplicated and could increase your compiled CSS file weight by a lot!

To avoid this, we can store all our icons into CSS variables and use a reference to the variable instead of outputting the encoded URI every time.

We will keep the same code we had before, but this time we will first output all the icons from the Sass list into the root of our webpage:

/**   * Convert all icons into custom properties   * They will be available to any HTML tag since they are attached to the :root   */  :root {   @each $  name, $  code in $  svg-icons {     --svg-#{$  name}: #{svg($  name)};   } }

Now, instead of calling the svg() function every time we need an icon, we have to use the variable that was created with the --svg prefix.

button::after {   /* Import inline SVG */   content: var(--svg-burger); }

Optimizing your SVGs

This technique does not provide any optimization on the source code of the SVG you are using. Make sure that you don’t leave unnecessary code; otherwise they will be encoded as well and will increase your CSS file size.

You can check this great list of tools and information on how to optimize properly your SVG. My favorite tool is Jake Archibald’s SVGOMG — simply drag your file in there and copy the outputted code.

Bonus: Updating the icon on hover

With this technique, we cannot select with CSS specific parts of the SVG. For example, there is no way to change the fill color of the icon when the user hovers the button. But there are a few tricks we can use with CSS to still be able to modify the look of our icon.

For example, if you have a black icon and you want to have it white on hover, you can use the invert() CSS filter. We can also play with the hue-rotate() filter.

That’s it!

I hope you find this little helper function handy in your own projects. Let me know what you think of the approach — I’d be interested to know how you’d make this better or tackle it differently!

How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , , ,

Logical Properties for Useful Shorthands

Michelle Barker with my favorite sorta blog post: short, practical, and leaves you with a valuable nugget for your time. Here, she gets into logical property shorthands in CSS, particularly those that set lengths just on a single axis, say only the block (vertical) axis or just the inline (horizontal) axis.

I say “block” and ”inline” because, as far as logical properties are concerned, the x-axis could just as well behave like a vertical axis depending on the current writing-mode.

So, where we’ve always had padding, margin, and border shorthands that can support a multi-value syntax, none of them allow us to declare lengths on a specific axis without also setting a length on the other axis.

For example:

/* This gives us margin on the inline axis */ margin: 0 3rem;

…but we had to set the other axis in order to get there. With logical properties, however, we have additional shorthands for each axis meaning we can cue up the margin-inline shorthand to work specifically on the inline axis:

margin-inline: 3rem;

Michelle mentions my favorite logical property shorthand in passing. How many times do you position something to this sort of tune:

.position-me {   position: absolute;   top: 0;   right: 0;   bottom: 0;   left: 0; }

We can get those four lines into in with inset: 0. Or we could target the block and inline axis directly with inset-block and inset-inline, respectively.

While we’re talking shorthands, I always like to put a word of caution about ”accidental” CSS resets. Just one of the common CSS mistakes I make.

To Shared LinkPermalink on CSS-Tricks

Logical Properties for Useful Shorthands originally published on CSS-Tricks. You should get the newsletter.


, , ,

Cool Hover Effects That Use Background Properties

A while ago, Geoff wrote an article about a cool hover effect. The effect relies on a combination of CSS pseudo-elements, transforms, and transitions. A lot of comments have shown that the same effect can be done using background properties. Geoff mentioned that was his initial thought and that’s what I was thinking as well. I am not saying the pseudo-element he landed on is bad, but knowing different methods to achieve the same effect can only be a good thing.

In this post, we will re-work that hover effect, but also expand it into other types of hover effects that only use CSS background properties.

You can see the background properties at work in that demo, as well as how we can use custom properties and the calc() function to do even more. We are going to learn how to combine all of these so we are left with nicely optimized code!

Hover effect #1

Let’s start with the first effect which is the reproduction of the one detailed by Geoff in his article. The code used to achieve that effect is the following:

.hover-1 {   background: linear-gradient(#1095c1 0 0) var(--p, 0) / var(--p, 0) no-repeat;   transition: .4s, background-position 0s; } .hover-1:hover {   --p: 100%;   color: #fff; }

If we omit the color transition (which is optional), we only need three CSS declarations to achieve the effect. You are probably surprised how small the code is, but you will see how we got there.

First, let’s start with a simple background-size transition:

We are animating the size of a linear gradient from 0 100% to 100% 100%. That means the width is going from 0 to 100% while the background itself remains at full height. Nothing complex so far.

Let’s start our optimizations. We first transform our gradient to use the color only once:

background-image: linear-gradient(#1095c1 0 0);

The syntax might look a bit strange, but we are telling the browser that one color is applied to two color stops, and that’s enough to define a gradient in CSS. Both color stops are 0, so the browser automatically makes the last one 100% and fills our gradient with the same color. Shortcuts, FTW!

With background-size, we can omit the height because gradients are full height by default. We can do a transition from background-size: 0 to background-size: 100%.

.hover-1 {   background-image: linear-gradient(#1095c1 0 0);   background-size: 0;   background-repeat: no-repeat;   transition: .4s; } .hover-1:hover {   background-size: 100%; }

Let’s introduce a custom property to avoid the repetition of background-size:

.hover-1 {   background-image: linear-gradient(#1095c1 0 0);   background-size: var(--p, 0%);   background-repeat: no-repeat;   transition: .4s; } .hover-1:hover {   --p: 100%; }

We are not defining --p initially, so the fallback value (0% in our case) will be used. On hover, we define a value that replaces the fallback one ( 100%).

Now, let’s combine all the background properties using the shorthand version to get:

.hover-1 {   background: linear-gradient(#1095c1 0 0) left / var(--p, 0%) no-repeat;   transition: .4s; } .hover-1:hover {   --p: 100%; }

We are getting closer! Note that I have introduced a left value (for the background-position) which is mandatory when defining the size in the background shorthand. Plus, we need it anyway to achieve our hover effect.

We need to also update the position on hover. We can do that in two steps:

  1. Increase the size from the right on mouse hover.
  2. Decrease the size from the left on mouse out.

To do this, we need to update the background-position on hover as well:

We added two things to our code:

  • A background-position value of right on hover
  • A transition-duration of 0s on the background-position

This means that, on hover, we instantly change the background-position from left (see, we needed that value!) to right so the background’s size will increase from the right side. Then, when the mouse cursor leaves the link, the transition plays in reverse, from right to left, making it appear that we are decreasing the background’s size from the left side. Our hover effect is done!

But you said we only needed three declarations and there are four.

That’s true, nice catch. The left and right values can be changed to 0 0 and 100% 0, respectively; and since our gradient is already full height by default, we can get by using 0 and 100%.

.hover-1 {   background: linear-gradient(#1095c1 0 0) 0 / var(--p, 0%) no-repeat;   transition: .4s, background-position 0s; } .hover-1:hover {   --p: 100%;   background-position: 100%; }

See how background-position and --p are using the same values? Now we can reduce the code down to three declarations:

.hover-1 {   background: linear-gradient(#1095c1 0 0) var(--p, 0%) / var(--p,0%) no-repeat;   transition: .4s, background-position 0s; } .hover-1:hover {   --p: 100%; }

The custom property --p is defining both the background position and size. On hover, It will update both of them as well. This is a perfect use case showing how custom properties can help us reduce redundant code and avoid writing properties more than once. We define our setting using custom properties and we only update the latter on hover.

But the effect Geoff described is doing the opposite, starting from left and ending at right. How do we do that when it seems we cannot rely on the same variable?

We can still use one variable and update our code slightly to achieve the opposite effect. What we want is to go from 100% to 0% instead of 0% to 100%. We have a difference of 100% that we can express using calc(), like this:

.hover-1 {   background: linear-gradient(#1095c1 0 0) calc(100% - var(--p,0%)) / var(--p,0%) no-repeat;   transition: .4s, background-position 0s; } .hover-1:hover {   --p: 100%; }

--p will change from 0% to 100%, but the background’s position will change from 100% to 0%, thanks to calc().

We still have three declarations and one custom property, but a different effect.

Before we move to the next hover effect, I want to highlight something important that you have probably noticed. When dealing with custom properties, I am using 0% (with a unit) instead of a unit-less 0. The unit-less zero may work when the custom property is alone, but will fail inside calc() where we need to explicitly define the unit. I may need another article to explain this quirk but always remember to add the unit when dealing with custom properties. I have two answers on StackOverflow (here and here) that go into more detail.

Hover effect #2

We need a more complex transition for this effect. Let’s take a look at a step-by-step illustration to understand what is happening.

Diagram showing the hover effect in three pieces.
Initially, a fixed-height, full-width gradient is outside of view. Then we move the gradient to the right to cover the bottom side. Finally, we increase the size of the gradient from the fixed height to 100% to cover the whole element.

We first have a background-position transition followed by a background-size one. Let’s translate this into code:

.hover-2 {   background-image: linear-gradient(#1095c1 0 0);   background-size: 100% .08em; /* .08em is our fixed height; modify as needed. */   background-position: /* ??? */;   background-repeat: no-repeat;   transition: background-size .3s, background-position .3s .3s; } .hover-2:hover {   transition: background-size .3s .3s, background-position .3s;   background-size: 100% 100%;   background-position: /* ??? */; }

Note the use of two transition values. On hover, we need to first change the position and later the size, which is why we are adding a delay to the size. On mouse out, we do the opposite.

The question now is: what values do we use for background-position? We left those blank above. The background-size values are trivial, but the ones for background-position are not. And if we keep the actual configuration we’re unable to move our gradient.

Our gradient has a width equal to 100%, so we cannot use percentage values on background-position to move it.

Percentage values used with background-position are always a pain especially when you use them for the first time. Their behavior is non-intuitive but well defined and easy to understand if we get the logic behind it. I think it would take another article for a full explanation why it works this way, but here’s another “long” explanation I posted over at Stack Overflow. I recommend taking a few minutes to read that answer and you will thank me later!

The trick is to change the width to something different than 100%. Let’s use 200%. We’re not worried about the background exceeding the element because the overflow is hidden anyway.

.hover-2 {   background-image: linear-gradient(#1095c1 0 0);   background-size: 200% .08em;   background-position: 200% 100%;   background-repeat: no-repeat;   transition: background-size .3s, background-position .3s .3s; } .hover-2:hover {   transition: background-size .3s .3s, background-position .3s;   background-size: 200% 100%;   background-position: 100% 100%; }

And here’s what we get:

It’s time to optimize our code. If we take the ideas we learned from the first hover effect, we can use shorthand properties and write fewer declarations to make this work:

.hover-2 {   background:      linear-gradient(#1095c1 0 0) no-repeat     var(--p, 200%) 100% / 200% var(--p, .08em);   transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s)); } .hover-2:hover {   --p: 100%;   --t: .3s; }

We add all the background properties together using the shorthand version then we use --p to express our values. The sizes change from .08em to 100% and the position from 200% to 100%

I am also using another variable --t , to optimize the transition property. On mouse hover we have it set to a .3s value, which gives us this:

transition: .3s .3s, background-position .3s 0s;

On mouse out, --t is undefined, so the fallback value will be used:

transition: .3s 0s, background-position .3s .3s;

Shouldn’t we have background-size in the transition?

That is indeed another optimization we can make. If we don’t specify any property it means “all” the properties, so the transition is defined for “all” the properties (including background-size and background-position). Then it’s defined again for background-position which is similar to defining it for background-size, then background-position.

“Similar” is different than saying something is the “same.” You will see a difference if you change more properties on hover, so the last optimization might be unsuitable in some cases.

Can we still optimize the code and use only one custom property?

Yes, we can! Ana Tudor shared a great article explaining how to create DRY switching where one custom property can update multiple properties. I won’t go into the details here, but our code can be revised like this:

.hover-2 {   background:      linear-gradient(#1095c1 0 0) no-repeat     calc(200% - var(--i, 0) * 100%) 100% / 200% calc(100% * var(--i, 0) + .08em);   transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - calc(var(--i, 0) * .3s)); } .hover-2:hover {   --i: 1; }

The --i custom property is initially undefined, so the fallback value, 0, is used. On hover though, we replace 0 with 1. You can do the math for both cases and get the values for each one. You can see that variable as a “switch” that update all our values at once on hover.

Again, we’re back to only three declarations for a pretty cool hover effect!

Hover effect #3

We are going to use two gradients instead of one for this effect. We will see that combining multiple gradients is another way to create fancy hover effects.

Here’s a diagram of what we’re doing:

We initially have two gradients that overflow the element so that they are out of view. Each one has a fixed height and toes up half of the element’s width. Then we slide them into view to make them visible. The first gradient is placed at the bottom-left and the second one at the top-right. Finally, we increase the height to cover the whole element.

Here’s how that looks in CSS:

.hover-3 {   background-image:     linear-gradient(#1095c1 0 0),     linear-gradient(#1095c1 0 0);   background-repeat: no-repeat;   background-size: 50% .08em;   background-position:     -100% 100%,     200% 0;   transition: background-size .3s, background-position .3s .3s; } .hover-3:hover {   background-size: 50% 100%;   background-position:     0 100%,     100% 0;     transition: background-size .3s .3s, background-position .3s; }

The code is almost the same as the other hover effects we’ve covered. The only difference is that we have two gradients with two different positions. The position values may look strange but, again, that’s related to how percentages work with the background-position property in CSS, so I highly recommend reading my Stack Overflow answer if you want to get into the gritty details.

Now let’s optimize! You get the idea by now — we’re using shorthand properties, custom properties, and calc() to tidy things up.

.hover-3 {   --c: no-repeat linear-gradient(#1095c1 0 0);   background:      var(--c) calc(-100% + var(--p, 0%)) 100% / 50% var(--p, .08em),     var(--c) calc( 200% - var(--p, 0%)) 0    / 50% var(--p, .08em);   transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s)); } .hover-3:hover {   --p: 100%;   --t: 0.3s; }

I have added an extra custom property, --c, that defines the gradient since the same gradient is used in both places.

I am using 50.1% in that demo instead of 50% for the background size because it prevents a gap from showing between the gradients. I also added 1% to the positions for similar reasons.

Let’s do the second optimization by using the switch variable:

.hover-3 {   --c: no-repeat linear-gradient(#1095c1 0 0);   background:      var(--c) calc(-100% + var(--i, 0) * 100%) 100% / 50% calc(100% * var(--i, 0) + .08em),     var(--c) calc( 200% - var(--i, 0) * 100%) 0 / 50% calc(100% * var(--i, 0) + .08em);   transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - var(--i, 0) * .3s); } .hover-3:hover {   --i: 1; }

Are you started to see the patterns here? It’s not so much that the effects we’re making are difficult. It’s more the “final step” of code optimization. We start by writing verbose code with a lot of properties, then reduce it following simple rules (e.g. using shorthand, removing default values, avoiding redundant values, etc) to simplify things down as much as possible.

Hover effect #4

I will raise the difficulty level for this last effect, but you know enough from the other examples that I doubt you’ll have any issues with this one.

This hover effect relies on two conic gradients and more calculations.

Initially, we have both gradients with zero dimensions in Step 1. We increase the size of each one in Step 2. We keep increasing their widths until they fully cover the element, as shown in Step 3. After that, we slide them to the bottom to update their position. This is the “magic” part of the hover effect. Since both gradients will use the same coloration, changing their position in Step 4 will make no visual difference — but we will see a difference once we reduce the size on mouse out during Step 5.

If you compare Step 2 and Step 5, you can see that we have a different inclination. Let’s translate that into code:

.hover-4 {   background-image:     conic-gradient(/* ??? */),     conic-gradient(/* ??? */);   background-position:     0 0,     100% 0;   background-size: 0% 200%;   background-repeat: no-repeat;   transition: background-size .4s, background-position 0s; } .hover-4:hover {   background-size: /* ??? */ 200%;   background-position:     0 100%,     100% 100%; }

The positions are pretty clear. One gradient starts at top left (0 0) and ends at bottom left (0 100%) while the other starts at top right (100% 0) and ends at bottom right (100% 100%).

We’re using a transition on the background positions and sizes to reveal them. We only need a transition value for the background-size. And like before, background-position needs to change instantly, so we’re assigning a 0s value for the transition’s duration.

For the sizes, both gradient need to have 0 width and twice the element height (0% 200%). We will see later how their sizes change on hover. Let’s first define the gradient configuration.

The diagram below illustrates the configuration of each gradient:

Note that for the second gradient (indicated in green), we need to know the height to use it inside the conic-gradient we’re creating. For this reason, I am going to add a line-height that sets the element’s height and then try that same value for the conic gradient values we left out.

.hover-4 {   --c: #1095c1;   line-height: 1.2em;   background-image:     conic-gradient(from -135deg at 100%  50%, var(--c) 90deg, #0000 0),     conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0);   background-position:     0 0,     100% 0;   background-size: 0% 200%;   background-repeat: no-repeat;   transition: background-size .4s, background-position 0s; } .hover-4:hover {   background-size: /* ??? */ 200%;   background-position:     0 100%,     100% 100%; }

The last thing we have left is to figure out the background’s size. Intuitively, we may think that each gradient needs to take up half of the element’s width but that’s actually not enough.

We’re left with a large gap if we use 50% as the background-size value for both gradients.

We get a gap equal to the height, so we actually need to do is increase the size of each gradient by half the height on hover for them to cover the whole element.

.hover-4:hover {   background-size: calc(50% + .6em) 200%;   background-position:     0 100%,     100% 100%; }

Here’s what we get after optimizing them like the previous examples:

.hover-4 {   --c: #1095c1;   line-height: 1.2em;   background:     conic-gradient(from -135deg at 100%  50%, var(--c) 90deg, #0000 0)        0  var(--p, 0%) / var(--s, 0%) 200% no-repeat,     conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0)        100% var(--p, 0%) / var(--s, 0%) 200% no-repeat;   transition: .4s, background-position 0s; } .hover-4:hover {   --p: 100%;   --s: calc(50% + .6em); } 

What about the version with only one custom property?

I will leave that for you! After looking at four similar hover effects, you should be able to get the final optimization down to a single custom property. Share your work in the comment section! There’s no prize, but we may end up with different implementations and ideas that benefit everyone!

Before we end, let me share a version of that last hover effect that Ana Tudor cooked up. It’s an improvement! But note that it lacks Firefox supports due to a known bug. Still, it’s a great idea that shows how to combine gradients with blend modes to create even cooler hover effects.

Wrapping up

We made four super cool hover effects! And even though they are different effects, they all take the same approach of using CSS background properties, custom properties, and calc(). Different combinations allowed us to make different versions, all using the same techniques that leave us with clean, maintainable code.

If you want to get some ideas, I made a collection of 500 (yes, 500!) hover effects, 400 of which are done without pseudo-elements. The four we covered in this article are just the tip of the iceberg!

Cool Hover Effects That Use Background Properties originally published on CSS-Tricks. You should get the newsletter.


, , , ,

Tricks to Cut Corners Using CSS Mask and Clip-Path Properties

We recently covered creating fancy borders with CSS mask properties, and now we are going to cut the corners with CSS mask and clip-path! A lot of techniques exist to cut different shapes from the corners of any element. In this article, we will consider modern techniques to create unique corner shapes while trying to work from reusable code that allows us to produce different results by adjusting variables.

Check this online tool to get an idea of what we are building. It’s a CSS generator where you select the shape, the corners, and the size then you get the code in no time!

We mainly have two types of cuts: a circular one and an angled one. For each, we can get the full shape or the border-only shape, not to mention that we can select the corners we want to cut. A lot of combinations!

Like in the previous article, we will make lots of use of the CSS mask property. So, if you are not familiar with it, I recommend reading the quick primer I wrote before continuing.

Circular cut-out

For a circular or rounded cut, we will use radial-gradient(). To cut four corners, the logical solution is to create four gradients, one for each corner:

Each gradient is taking a quarter of the element’s dimensions. The syntax of the gradient is self-explanatory:

radial-gradient(circle 30px at top left, #0000 98%, red) top left;

Translated, this renders a circle at the top-left corner with a 30px radius. The main color is transparent (#0000) and the remaining is red. The whole gradient is also placed so that it starts at the element’s top-left corner. Same logic for the three other gradients. The keyword circle can be omitted since we explicitly specified one value for the radius.

Like I did in the previous article, I will be using slightly bigger or smaller values this time around in order to avoid bad visual result. Here, I am using 98% instead of 100% to avoid jagged edges and 51% instead of 50% to create an overlap between gradients and avoid white spaces. This logic will follow throughout this article. In fact, you will find that adding or removing 1% or 1deg typically results in a nice visual.

We apply this to the CSS mask property and we are done!

We can actually optimize that code a little:

--g: #0000 98%,#000; --r: 30px; mask:   radial-gradient(var(--r) at 0    0   ,var(--g)) 0    0,   radial-gradient(var(--r) at 100% 0   ,var(--g)) 100% 0,   radial-gradient(var(--r) at 0    100%,var(--g)) 0    100%,   radial-gradient(var(--r) at 100% 100%,var(--g)) 100% 100%; mask-size: 51% 51%; mask-repeat: no-repeat;

This way, we use custom properties for the redundant values and, as a personal preference, I am using numeric values for the positions instead of keywords.

In the generator, I will use the following syntax:

--g: #0000 98%,#000; --r: 30px; mask:   radial-gradient(var(--r) at 0    0   ,var(--g)) 0    0   /51% 51% no-repeat,   radial-gradient(var(--r) at 100% 0   ,var(--g)) 100% 0   /51% 51% no-repeat,   radial-gradient(var(--r) at 0    100%,var(--g)) 0    100%/51% 51% no-repeat,   radial-gradient(var(--r) at 100% 100%,var(--g)) 100% 100%/51% 51% no-repeat;

The shorthand syntax is easier to generate plus the whole value can be used as one custom property.

Can we use fewer gradients if we want?

Sure! One gradient can do the job. Hover the below to see the trick:

Here, we define one radial-gradient() with no size (by default it is 100% height and 100% width). This gives us a hole in the center. We translate/move the gradient by half the width and height of the image to move the hole to one corner. Since, by default, the CSS mask repeats, we get the same on each corner. We have four cut corners with only one gradient!

The only drawback of this method is that we need to know the width and height of the element in advance.

Can’t we use -50% instead of half the width and height?

Unfortunately, we’re unable to do that here because percentages doesn’t behave the same as pixel values when used with the CSS mask-position property. They’re tricky.

I have a detailed Stack Overflow answer that explains the difference. It deals with background-position but the same logic applies to the CSS mask-position property.

However, we can use some tricks to make it work with percentage values and without the need to know the width or the height. When a gradient (or a background layer) has a width and height equal to the element, we cannot move it using percentage values. So we need to change its size!

I will define a size equal to 99.5% 99.5%. I am reducing 0.5% from the width and the height to have a value different from 100% and at the same time keep the same visual result since we won’t notice a big difference between 100% and 99.5%. Now that our gradient has a size different from 100% we can move it using percentage values.

I will not detail all the math, but to move it by half the width and the height we need to use this equation:

100% * (50/(100 - 99.5)) = 100% * 100 = 10000%

It’s a strange value but it does the job:

As you can see, the trick works just fine. Whatever the size of the element is, we can cut four corners using only one gradient. However, this method has a small drawback when the width or the height of the element is a decimal value. Here is an example with an image having a width equal to 150.5px:

The use of 99.5% combined with 150.5px will create rounding issues that will break the calculation, resulting in the mask being misaligned. So, use this method with caution.

To overcome the rounding issue, we can combine the last trick with a pseudo-element. Here is a step-by-step illustration to understand the idea:

Here’s what going on in there:

  1. We define a pseudo-element that behaves as our background layer. Logically, we should use inset:0 to make it cover the entire area, but we will create a small overflow by using inset: -10% meaning that the pseudo element will overflow each side by 10%.
  2. We set our CSS mask to the pseudo-element. The mask size needs to match the size of the main element, not the pseudo-element. In other words, it will be smaller than the size of the pseudo-element and this is what we want to be able to move using percentage values. After we do the math, the size needs to be 100%/1.2. Notice in the demo above that the CSS mask is within the green border so that it matches the size of the container.
  3. Now, we need to move it in a way that simulates cutting the corner of the main element. The center of the hole needs to be in the corner of the main element, as illustrated in the demo. To do this, we use mask-position: 300% 300% ( 300% = 50%/(1 - 1/1.2) ).
  4. We remove no-repeat to activate the repetition and get the same effect for every corner.
  5. We clip the overflow and we get our final result!

I know it’s a bit overkill, but it does work and it requires only one gradient instead of four.

Let’s quickly recap the three methods we just covered:

  • The first method uses four gradients and has no drawbacks as far as usage. Sure, it’s verbose but it works with any kind of element and size. I recommend using this one.
  • The second method uses one gradient and works with any element, but it can break in some particular cases. It’s suitable with fixed-size elements. It’s ok to use, but maybe less frequently.
  • The third method uses one gradient and requires a pseudo-element. It won’t work with <img> and other elements that unable to support a pseudo-element.

The generator only supports the first and third methods.

Now that we saw the case with all the corners, let’s disable some of them. Using the first method, any corner we want to keep uncut we simply remove its gradient and adjust the size of what remains.

To disable the top-right corner:

  • We remove the top-right gradient (the blue one).
  • We have an empty corner, so we increase the size of the red gradient (or the purple one) to cover that leftover space.


You probably see just how many possibilities and combinations we can do here. If we want to cut N corners (where N ranges from 1 to 4), we use N gradients. All we need is to correctly set the size of each one to leave no space.

What about the other methods where there’s only one gradient? We will need another gradient! Those two methods use only one radial-gradient() to cut the corners, so we will rely on another gradient to “hide” the cut. We can use a conic-gradient() with four sections for this task:

conic-gradient(red 25%, blue 0 50%, green 0 75%, purple 0)

We add it on the top of the radial gradient to get the following:

The conic-gradient() covers the radial-gradient() and no corner is cut. Let’s change one color in the conic-gradient() to transparent. The one at the top-right, for example:

Did you see that? We revealed one corner of the radial-gradient() and we end with one cut corner!

Now let’s do the same thing, but for the bottom-left corner.

I think you probably get the trick by now. By changing the colors of the conic-gradient() from opaque to transparent, we reveal the corners we want to cut and gain all kinds of possible combinations. The same can be done with the third method.

Circular border-only cut-out

Let’s make the border-only version of the previous shape. In other words, we achieve the same shape but knock out the fill so all we’re left with is a border of the shape.

This is a bit tricky because we have different cases with different code. Fair warning, I will be using a lot of gradients here while finding opportunities to trim the number of them.

It should be noted that we will consider a pseudo-element in this case. Showing only the border means we need to hide the inner “fill” of the shape. Applying this to the main element will also hide the content — that’s why this is a nice use case for a pseudo-element.

One cut corner

This one needs one radial gradient and two conic gradients:

The first example illustrates the radial gradient (in red) and both conic gradients (in blue and green). In the second example, we apply all of them inside the CSS mask property to create the border-only shape with one cut corner.

Diagram zoomed in on two corners of a rectangle and another where the CSS mask is placed.
Here’s a diagram of the game plan.

As the diagram shows, the radial-gradient() creates the quarter of a circle and each conic-gradient() creates two perpendicular segments to cover two sides. It should be noted that overlapping gradients is not an issue since we are not going to change the CSS mask-composite property value.

Using the same code an adjusting a few variables, we can get the shape for the other corners.

Two cut corners

For the two-corner configuration we have two situations taking place.

In the first situation, there are two opposite corners where we need two radial gradients and two conic gradients.

The configuration is almost the same as cutting only one corner: we add an extra gradient and update a few variables.

In the second situation, there are two adjacent corners and, in this case, we need two radial gradients, one conic gradient, and one linear gradient.

“Wait!” you might exclaim. “How come the conic gradient covers three sides?” If you check the code, notice the repeat-y. In all of the examples, we always used no-repeat for the gradients, but for this we can repeat one of them to cover more sides and reduce the number of gradients we use.

Here is an example with only the conic-gradient() to understand the repetition. The trick is to have a height equal to 100% minus the border size so that the gradient fills that space when repeating, which covers the third side in the process.

Three cut corners

For this configuration, we need three radial gradients, one conic gradient, and two linear gradients.

Four corners cut

It takes four radial gradients and two linear gradients to cut all four corners.

I can hear you screaming, “How the heck am I supposed to memorize all these cases?!” You don’t need to memorize anything since you can easily generate the code for each case using the online generator. All you need is to understand the overall trick rather than each individual case. That’s why I’ve only gone into fine detail on the first configurations — the rest are merely iterations that tweak the initial foundation of the trick.

Notice there’s a general pattern we’ve been following throughout the examples:

  1. We add a radial-gradient() on the corners we want to cut.
  2. We fill the sides using either a conic-gradient() or a linear-gradient() to create the final shape.

It should be noted that we can find different ways to create the same shape. What I am showing in this post are the methods I found to be best after trying lots of other ideas. You may have a different approach you consider to be better! If so, definitely share it in the comments!

Angled cut-out

Let’s tackle another type of cut shape: the angled cut.

We have two parameters: the size and angle of the cut. To get the shape, we need a conic-gradient() for each corner. This configuration is very similar to the example that kicked off this article.

Here is an illustration of one corner to understand the trick:

The difference between each corner is an extra offset of 90deg in from and the at position. The full code is like below:

--size: 30px; --angle: 130deg;  --g: #0000 var(--angle), #000 0; mask:   conic-gradient(from calc(var(--angle)/-2 -  45deg)      at top    var(--size) left  var(--size),var(--g)) top left,   conic-gradient(from calc(var(--angle)/-2 + 45deg)      at top    var(--size) right var(--size),var(--g)) top right,   conic-gradient(from calc(var(--angle)/-2 - 135deg)      at bottom var(--size) left  var(--size),var(--g)) bottom left,   conic-gradient(from calc(var(--angle)/-2 + 135deg)      at bottom var(--size) right var(--size),var(--g)) bottom right; mask-size: 51% 51%; mask-repeat: no-repeat;

If we want to disable one corner, we remove the conic-gradient() for that corner and update the size of another one to fill the remaining space exactly like we did with the circular cut. Here’s how that looks for one corner:

We can do the exact same thing for all the other corners to get the same effect.

In addition to CSS mask, we can also use the CSS clip-path property to cut the corners. Each corner can be defined with three points.

Zooming in on a corner of the shape showing the three points that form the angled cut.
The shape consists of two points at each end of the cut, and one between them to form the angle.

The other corners will have the same value with an offset of 100%. This gives us the final code with a total of 12 points — three per corner.

/* I will define T = [1-tan((angle-90)/2)]*size */ clip-path: polygon(   /* Top-left corner */   0 T, size size,0 T, /* OR 0 0 */   /* Top-right corner */   calc(100% - T) 0,calc(100% - size) size,100% T, /* OR  100% 0 */   /* Bottom-right corner*/   100% calc(100% - T),calc(100% - size) calc(100% - size), calc(100% - T) 100%, /* OR 100% 100% */   /* Bottom-left corner */    T 100%, size calc(100% - size),0 calc(100% - T) /* OR 0 100% */ )

Notice the OR comments in that code. It defines the code we have to consider if we want to disable a particular corner. To cut a corner, we use three points. To uncut a corner, we use one point — which is nothing but the coordinate of that corner.

Border-only angled cut

Oof, we have reached the last and trickiest shape at last! This one can be achieved with either gradients or clip-path, but let’s go with the clip-path approach.

Things would get complex and verbose if we go with the gradient approach. Here’s a demo that illustrates that point:

There are nine gradients total, and I am still not done with the calculation. As you can tell, the thickness of the border is incorrect, plus the final result is unsatisfying due to the nature of gradients and their anti-aliasing issues. This approach might be a good exercise to push the limit of gradients, but I don’t recommend it in a production environment.

So, back to the clip-path method. We will still wind up with verbose code, but less of a big deal since the generator can do the job for us with a cleaner end result.

Here is an overview of the path. I am adding a small gap to better see the different points but we should have an overlap of points instead.

We have 13 outer points (the ones in black) and 13 inner points (the ones in blue).

The way we calculate the outer points is the same as how we did it for the regular angled cut. For the inner points, however, we need more math. Don’t worry, I’ll spare you some “boring” geometry explanation for this one. I know most of you don’t want it, but in case you need to dig into this, you can check the JavaScript file of the generator to find the code and the math I am using to generate the shape.

The 180deg special case

Before we end, there’s a special case for the angle cut I want to call out. It’s where we use an angle equal to 180deg. Here’s what that produces:

We have a straight line on the corner so we can optimize the clip-path code. For the full shape, we can use eight points (two points per corner) instead of 12. And for the border-only version, we can use 18 points (nine inner points and outer points) instead of 26. In other words, we can remove the middle point.

The border-only shape can also be made using gradients. But rather than using nine gradients like we did before, we can get away with only four linear gradients and a clean result.


We just combined CSS masks with gradients to create some fancy shapes without resorting to hacks and a lot of code! We also experienced just how much it takes to strike the right balance of code to get the right results. We even learned a few tricks along the way, like changing values by one or even half a unit. CSS is super powerful!

But, as we discussed, the online generator I made is a great place to get the code you need rather than writing it out by hand. I mean, I went through all the work of figuring out how all of this works and I would likely still need to reference this very article to remember how it’s all put together. If you can memorize all of this, kudos! But it’s nice to have a generator to fall back on.

Tricks to Cut Corners Using CSS Mask and Clip-Path Properties originally published on CSS-Tricks. You should get the newsletter.


, , , , ,

Multi-Value CSS Properties With Optional Custom Property Values

Imagine you have an element with a multi-value CSS property, such as transform: optional custom property values:

.el {   transform: translate(100px) scale(1.5) skew(5deg); }

Now imagine you don’t always want all the transform values to be applied, so some are optional. You might think of CSS optional custom property values:

.el {   /*         |-- default ---| |-- optional --| */   transform: translate(100px) var(--transform); }

But surprisingly using optional custom property values like this does not work as intended. If the --transform variable is not defined the whole property will not be applied. I’ve got a little “trick” to fix this and it looks like this:

.el {   transform: translate(100px) var(--transform, ); }

Notice the difference? There is a fallback defined in there that is set to an empty value: (, )

That’s the trick, and it’s very useful! Here’s what the specification has to say:

In an exception to the usual comma elision rules, which require commas to be omitted when they’re not separating values, a bare comma, with nothing following it, must be treated as valid in var(), indicating an empty fallback value.

This is somewhat spiritually related to the The CSS Custom Property Toggle Trick that takes advantage of a custom property having the value of an empty space.


Like I said, this is useful and works for any multi-value CSS property. The following demo shows it using text-shadow, background, and filter in addition to the transform example we just discussed.

See the Pen CSS var – Fallback To Nothing by Yair Even Or (@vsync) on CodePen.

Some properties that accept multiple values, like text-shadow, require special treatment because they only work with a comma delimiter. In those cases, when the CSS custom property is defined, you (as the code author) know it is only to be used in a situation where a value is already defined where the custom property is used. Then a comma should be inserted directly in the custom property before the first value, like this:

--text-shadow: ,0 0 5px black;

This, of course, inhibits the ability to use this variable in places where it’s the only value of some property. That can be solved, though, by creating “layers” of variables for abstraction purposes, i.e. the custom property is pointing to lower level custom properties.

Beware of Sass compiler

While exploring this trick, uncovered a bug in the Sass compiler that strips away the empty value (,) fallback, which goes against the spec. I’ve reported the bug and hope it will be fixed up soon.

As a temporary workaround, a fallback that causes no rendering can be used, such as:

transform: translate(100px) var(--transform, scale(1));

Multi-Value CSS Properties With Optional Custom Property Values originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , , , ,

Honor prefers-color-scheme in the CSS Paint API with Custom Properties

One of the coolest things I’ve been messing with in the last couple years is the CSS Paint API. I love it. I did a talk on it, and made a little gallery of my own paint worklets. The other cool thing is the prefers-color-scheme media query and how you can use it to adapt to a user’s preference for light or dark modes.

Recently, I found out that I can combine both of these really cool things with CSS custom properties in such a way that a paint worklet’s appearance can be tailored to fit the user’s preferred color scheme!

Setting the stage

I’ve been overdue for a website overhaul, and I decided to go with a Final Fantasy II theme. My first order of business was to make a paint worklet that was a randomly generated Final Fantasy-style landscape I named overworld.js:

An 8-bit illustration landscape of a forest with scattered pine trees and a jagged river running through the green land.
A randomly generated 8-bit style landscape, made possible by the CSS Paint API!

It could use a bit more dressing up—and that’s certainly on the agenda—but this here is a damn good start!

After I finished the paint worklet, I went on to work on other parts of the website, such as a theme switcher for light and dark modes. It was then that I realized that the paint worklet wasn’t adapting to these preferences. This might normally be a huge pain, but with CSS custom properties, I realized I could adapt the paint worklet’s rendering logic to a user’s preferred color scheme with relative ease!

Setting up the custom properties for the paint worklet

The state of CSS these days is pretty dope, and CSS custom properties are one such example of aforementioned dopeness. To make sure both the Paint API and custom properties features are supported, you do a little feature check like this:

const paintAPISupported = "registerProperty" in window.CSS && "paintWorklet" in window.CSS`

The first step is to define your custom properties, which involves the CSS.registerProperty method. That looks something like this:

CSS.registerProperty({   name,             // The name of the property   syntax,           // The syntax (e.g., <number>, <color>, etc.)   inherits,         // Whether the value can be inherited by other properties   initialValue      // The default value });

Custom properties are the best part of using the Paint API, as these values are specified in CSS, but readable in the paint worklet context. This gives developers a super convenient way to control how a paint worklet is rendered—entirely in CSS.

For the overworld.js paint worklet, the custom properties are used to define the colors for various parts of the randomly generated landscape—the grass and trees, the river, the river banks, and so on. Those color defaults are for the light mode color scheme.

The way I register these properties is to set up everything in an object that I call with Object.entries and then loop over the entries. In the case of my overworld.js paint worklet, that looked like this:

// Specify the paint worklet's custom properties const properties = {   "--overworld-grass-green-color": {     syntax: "<color>",     initialValue: "#58ab1d"   },   "--overworld-dark-rock-color": {     syntax: "<color>",     initialValue: "#a15d14"   },   "--overworld-light-rock-color": {     syntax: "<color>",     initialValue: "#eba640"   },   "--overworld-river-blue-color": {     syntax: "<color>",     initialValue: "#75b9fd"   },   "--overworld-light-river-blue-color": {     syntax: "<color>",     initialValue: "#c8e3fe"   } };  // Register the properties Object.entries(properties).forEach(([name, { syntax, initialValue }]) => {   CSS.registerProperty({     name,     syntax,     inherits: false,     initialValue   }); });  // Register the paint worklet CSS.paintWorklet.addModule("/worklets/overworld.js");

Because every property sets an initial value, you don’t have to specify any custom properties when you call the paint worklet later. However, because the default values for these properties can be overridden, they can be adjusted when users express a preference for a color scheme.

Adapting to a user’s preferred color scheme

The website refresh I’m working on has a settings menu that’s accessible from the site’s main navigation. From there, users can adjust a number of preferences, including their preferred color scheme:

The color scheme setting cycles through three options:

  • System
  • Light
  • Dark

“System” defaults to whatever the user has specified in their operating system’s settings. The last two options override the user’s operating system-level setting by setting a light or dark class on the <html> element, but in the absence of an explicit, the “System” setting relies on whatever is specified in the prefers-color-scheme media queries.

The hinge for this override depends on CSS variables:

/* Kicks in if the user's site-level setting is dark mode */ html.dark {    /* (I'm so good at naming colors) */   --pink: #cb86fc;   --firion-red: #bb4135;   --firion-blue: #5357fb;   --grass-green: #3a6b1a;   --light-rock: #ce9141;   --dark-rock: #784517;   --river-blue: #69a3dc;   --light-river-blue: #b1c7dd;   --menu-blue: #1c1f82;   --black: #000;   --white: #dedede;   --true-black: #000;   --grey: #959595; }  /* Kicks in if the user's system setting is dark mode */ @media screen and (prefers-color-scheme: dark) {   html {     --pink: #cb86fc;     --firion-red: #bb4135;     --firion-blue: #5357fb;     --grass-green: #3a6b1a;     --light-rock: #ce9141;     --dark-rock: #784517;     --river-blue: #69a3dc;     --light-river-blue: #b1c7dd;     --menu-blue: #1c1f82;     --black: #000;     --white: #dedede;     --true-black: #000;     --grey: #959595;   } }  /* Kicks in if the user's site-level setting is light mode */ html.light {   --pink: #fd7ed0;   --firion-red: #bb4135;   --firion-blue: #5357fb;   --grass-green: #58ab1d;   --dark-rock: #a15d14;   --light-rock: #eba640;   --river-blue: #75b9fd;   --light-river-blue: #c8e3fe;   --menu-blue: #252aad;   --black: #0d1b2a;   --white: #fff;   --true-black: #000;   --grey: #959595; }  /* Kicks in if the user's system setting is light mode */ @media screen and (prefers-color-scheme: light) {   html {     --pink: #fd7ed0;     --firion-red: #bb4135;     --firion-blue: #5357fb;     --grass-green: #58ab1d;     --dark-rock: #a15d14;     --light-rock: #eba640;     --river-blue: #75b9fd;     --light-river-blue: #c8e3fe;     --menu-blue: #252aad;     --black: #0d1b2a;     --white: #fff;     --true-black: #000;     --grey: #959595;   } }

It’s repetitive—and I’m sure someone out there knows a better way—but it gets the job done. Regardless of the user’s explicit site-level preference, or their underlying system preference, the page ends up being reliably rendered in the appropriate color scheme.

Setting custom properties on the paint worklet

If the Paint API is supported, a tiny inline script in the document <head> applies a paint-api class to the <html> element.

/* The main content backdrop rendered at a max-width of 64rem.    We don't want to waste CPU time if users can't see the    background behind the content area, so we only allow it to    render when the screen is 64rem (1024px) or wider. */ @media screen and (min-width: 64rem) {   .paint-api .backdrop {     background-image: paint(overworld);     position: fixed;     top: 0;     left: 0;     width: 100%;     height: 100%;     z-index: -1;      /* These oh-so-well-chosen property names refer to the        theme-driven CSS variables that vary according to        the user's preferred color scheme! */     --overworld-grass-green-color: var(--grass-green);     --overworld-dark-rock-color: var(--dark-rock);     --overworld-light-rock-color: var(--light-rock);     --overworld-river-blue-color: var(--river-blue);     --overworld-light-river-blue-color: var(--light-river-blue);   } }

There’s some weirdness here for sure. For some reason, that may or may not be the case later on—but is at least the case as I write this—you can’t render a paint worklet’s output directly on the <body> element.

Plus, because some pages can be quite tall, I don’t want the entire page’s background to be filled with randomly generated (and thus potentially expensive) artwork. To get around this, I render the paint worklet in an element that uses fixed positioning that follows the user as they scroll down, and occupies the entire viewport.

All quirks aside, the magic here is that the custom properties for the paint worklet are based on the user’s system—or site-level—color scheme preference because the CSS variables align with that preference. In the case of the overworld paint worklet, that means I can adjust its output to align with the user’s preferred color scheme!

Not bad! But this isn’t even that inventive of a way to control how paint worklets render. If I wanted, I could add some extra details that would only appear in a specific color scheme, or do other things to radically change the rendering or add little easter eggs. While I learned a lot this year, I think this intersection of APIs was one of my favorites.


, , , ,

Standardizing Focus Styles With CSS Custom Properties

Take two minutes right now and visit your current project in a browser. Then, using only the Tab key, you should be able to navigate between interactive elements including buttons, links, and form elements.

If you are sighted, you should be able to visually follow the focus as it jumps between elements in the DOM. But if you do not see any visual change, or only a barely noticeable visual change, then you’ve found the one thing you can do to make a big difference for your visitors.

We’re going to look at a technique to make your focus styles more manageable across your project by using CSS custom properties and learn about a modern CSS focus selector. But first, let’s learn more about why visible focus styles are important.

Meeting WCAG Focus Style Criteria

Visible focus states are covered in the Web Content Accessibility Guidelines (WCAG) Success Criterion 2.4.7 – Focus Visible. The Understanding doc for 2.4.7 states the following in the intent of this criteria:

The purpose of this success criterion is to help a person know which element has the keyboard focus. It must be possible for a person to know which element among multiple elements has the keyboard focus.

In the upcoming WCAG 2.2, a new criterion is being added to clarify “how visible the focus indicator should be.” While currently in draft, getting familiar with and applying the guidelines in 2.4.11 – Focus Appearance (Minimum) is definitely a positive step you can take today to improve your focus styles.

Managing focus style with CSS custom properties

A technique I’ve started using this year is to include the following setup early in my cascade on the primary base interactive elements:

:is(a, button, input, textarea, summary) {   --outline-size: max(2px, 0.08em);   --outline-style: solid;   --outline-color: currentColor; }  :is(a, button, input, textarea, summary):focus {   outline: var(--outline-size) var(--outline-style) var(--outline-color);   outline-offset: var(--outline-offset, var(--outline-size)); }

This attaches custom properties that allow you the flexibility to customize just parts of the outline style as needed to ensure the focus remains visible as the element’s context changes.

For --outline-size, we’re using max() to ensure at least a value of 2px, while allowing the possibility of scaling relative to the component (ex. a large button or link within a headline) based on 0.08em.

A property you might not be familiar with here is outline-offset which defines the space between the element and the outline. You can even provide a negative number to inset the outline, which can be very useful for ensuring contrast of the focus style. In our rule set, we’ve set that property to accept an optional custom property of --outline-offset so that it can be customized if needed, but otherwise it has the fallback to match the --outline-size.

Improvements for outline appearance

Over my career, I’ve both been asked to remove outlines and removed them myself because they were considered “ugly”.

There are now two reasons outline should absolutely never have cause to be removed (in addition to the accessibility impact):

  1. outline now follows border-radius in Chromium and Firefox! 🎉 This means you can considering removing any hacks you may have used, such as faking it with a box-shadow (which has another positive accessibility impact of ensuring focus styles aren’t removed for Windows High Contrast Theme users).
  2. Using :focus-visible we can ask the browser to use heuristics to only show focus styles when it detects input modalities that require visible focus. Simplified, that means mouse users won’t see them on click, keyboard users will still have them on tab.

It’s important to note that form elements always show a focus style — they are exempt from the behavior of :focus-visible.

So let’s enhance our rule set to add the following to include :focus-visible. We’ll keep the initial :focus style we already defined for older browsers so that it’s not lost just in case.

:is(a, button, input, textarea, summary):focus-visible {   outline: var(--outline-size) var(--outline-style) var(--outline-color);   outline-offset: var(--outline-offset, var(--outline-size)); }

Due to the way browsers throw out selectors they don’t understand, we do need to make these separate rules and not combine them even though they define the same outline properties.

Finally, we also need this kind of funny-looking :focus:not(:focus-visible) rule that removes the regular focus styles for browsers that support :focus-visible:

:is(a, button, input, textarea, summary):focus:not(:focus-visible) {   outline: none; }

Of note is that the latest versions of Chromium and Firefox have switched to using :focus-visible as the default way to apply focus styles on interactive elements, and just recently was enabled as default in webkit so it should be in Safari stable soon! Our rules are still valid since we’re customizing the outline appearance.

For more guidance on visible focus styles, I recommend Sara Soueidan’s amazing and thorough guide to focus indicators because it considers the upcoming 2.4.11 criterion.

Focus styles demo

This Pen shows examples of each of these interactive elements and how to apply customizations using the custom properties, including a few swaps for dark mode. Depending on your browser support, you may not see a focus style due to :focus-visible unless you use the tab key.

One final note: button is a unique interactive element when it comes to focus styles because it has additional considerations across its states, particularly if you are relying on color alone. For help with that, try out the palette generator from my project ButtonBuddy.dev.


, , , ,

Open Props (and Custom Properties as a System)

Perhaps the most basic and obvious use of CSS custom properties is design tokens. Colors, fonts, spacings, timings, and other atomic bits of design that you can pull from as you design a site. If you pretty much only pull values from design tokens, you’ll be headed toward clean design and that consistent professional look that is typically the goal in web design. In fact, I’ve written that I think it’s exactly this that contributes to the popularity of utility class frameworks:

I’d argue some of that popularity is driven by the fact that if you choose from these pre-configured classes, that the design ends up fairly nice. You can’t go off the rails. You’re choosing from a limited selection of values that have been designed to look good.

I’m saying this (with a stylesheet that defines these classes as one-styling-job tokens):

<h1 class="color-primary size-large">Header<h1>

…is a similar value proposition as this:

html {   --color-primary: green;   --size-large: 3rem;   /* ... and a whole set of tokens */ }  h1 {   color: var(--color-primary);   font-size: var(--size-large); }

There are zero-build versions of both. For example, Tachyons is an it-is-what-it is stylesheet with a slew of utility classes you just use, while Windi is a whole fancy thing with a just-in-time compiler and such. Pollen is an it-is-what-it is library of custom properties you just use, while the brand new Open Props has a just-in-time compiler to only deliver the custom properties that are used.

Right, so, Open Props!

The entire thing is literally just a whole pile of CSS custom properties you can use to design stuff. It’s like a massive starting point for your styles. It’s saying custom property all the things, but in the way that we’re already used to with design tokens where they are a limited pre-determined number of choices.

The analogies are clear to people:

My guess is what will draw people to this is the beautiful defaults.

What it doesn’t do is prevent you from having to name things, which is something I know utility-class lovers really enjoy. Here, you’ll need to continue to use regular ol’ CSS selectors (like with named classes) to select things and style them as you “normally” would. But rather than hand-crafting your own values, you’re plucking values from these custom properties.

The whole base thing (you can view the source here) rolls in at 4.4kb across the wire (that’s what my DevTools showed, anyway). That doesn’t include the CSS you write to use the custom properties, but it’s a pretty tiny amount of overhead. There are additional PropPacks that increase the size (but thye are also super tiny), and if you’re worried about size, that’s what the whole just-in-time thing is about. You can play with that on StackBlitz.

Seems pretty sweet to me! I’d use it. I like that it’s ultimately just regular CSS, so there is nothing you can’t do. You’ll stay in good shape as CSS evolves.


, , , ,

Parallax Powered by CSS Custom Properties

Good friend Kent C. Dodds has recently dropped his new website which had a lot of work go into it. I was fortunate enough that Kent reached out a while back and asked if I could come up with some “whimsy” for the site. ✨

One of the first things that drew my attention was the large image of Kody (🐨) on the landing page. He’s surrounded by objects and that, to me, screamed, “Make me move!”

Life-like illustration of an animatronic panda in a warn jacket and riding a snowboard while surrounded by a bunch of objects, like leaves, skis, and other gadgets.

I have built parallax-style scenes before that respond to cursor movement, but not to this scale and not for a React application. The neat thing about this? We can power the whole thing with only two CSS custom properties.

Let’s start by grabbing our user’s cursor position. This is as straightforward as:

const UPDATE = ({ x, y }) => {   document.body.innerText = `x: $ {x}; y: $ {y}` } document.addEventListener('pointermove', UPDATE)

We want to map these values around a center point. For example, the left side of the viewport should be -1 for x, and 1 for the right side. We can reference an element and work out the value from its center using a mapping function. In this project, I was able to use GSAP and that meant using some of its utility functions. They already provide a mapRange() function for this purpose. Pass in two ranges and you’ll get a function you can use to get the mapped value.

const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => {   const INPUT_RANGE = inputUpper - inputLower   const OUTPUT_RANGE = outputUpper - outputLower   return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0) } // const MAPPER = mapRange(0, 100, 0, 10000) // MAPPER(50) === 5000

What if we want to use the window as the container element? We can map the value to the width and height of it.

import gsap from 'https://cdn.skypack.dev/gsap'  const BOUNDS = 100  const UPDATE = ({ x, y }) => {   const boundX = gsap.utils.mapRange(0, window.innerWidth, -BOUNDS, BOUNDS, x)   const boundY = gsap.utils.mapRange(0, window.innerHeight, -BOUNDS, BOUNDS, y)   document.body.innerText = `x: $ {Math.floor(boundX) / 100}; y: $ {Math.floor(boundY) / 100};` }  document.addEventListener('pointermove', UPDATE)

That gives us a range of x and y values that we can plug into our CSS. Note how we are dividing the values by 100 to get a fractional value. This should make sense when we integrate these values with our CSS a little later.

Now, what if we have an element that we want to map that value against, and within a certain proximity? In other words, we want our handler to look up the position of the element, work out the proximity range, and then map the cursor position to that range. The ideal solution here is to create a function that generates our handler for us. Then we can reuse it. For the purpose of this article, though, we’re operating on a “happy path” where we are avoiding type checks or checking for the callback value, etc.

const CONTAINER = document.querySelector('.container')  const generateHandler = (element, proximity, cb) => ({x, y}) => {   const bounds = 100   const elementBounds = element.getBoundingClientRect()   const centerX = elementBounds.left + elementBounds.width / 2   const centerY = elementBounds.top + elementBounds.height / 2   const boundX = gsap.utils.mapRange(centerX - proximity, centerX + proximity, -bounds, bounds, x)   const boundY = gsap.utils.mapRange(centerY - proximity, centerY + proximity, -bounds, bounds, y)   cb(boundX / 100, boundY / 100) }  document.addEventListener('pointermove', generateHandler(CONTAINER, 100, (x, y) => {   CONTAINER.innerText = `x: $ {x.toFixed(1)}; y: $ {y.toFixed(1)};` }))

In this demo, our proximity is 100. We’ll style it with a blue background to make it obvious. We pass a callback that gets fired each time the values for x and y get mapped to the bounds. We can divide these values in the callback or do what we want with them.

But wait, there’s an issue with that demo. The values go outside the bounds of -1 and 1. We need to clamp those values. GreenSock has another utility method we can use for this. It’s the equal of using a combination of Math.min and Math.max. As we already have the dependency, there’s no point in reinventing the wheel! We could clamp the values in the function. But, choosing to do so in our callback will be more flexible as we’ll show coming up.

We could do this with CSS clamp() if we’d like. 😉

document.addEventListener('pointermove', generateHandler(CONTAINER, 100, (x, y) => {   CONTAINER.innerText = `     x: $ {gsap.utils.clamp(-1, 1, x.toFixed(1))};     y: $ {gsap.utils.clamp(-1, 1, y.toFixed(1))};   ` }))

Now we have clamped values!

In this demo, adjust the proximity and drag the container around to see how the handler holds up.

That’s the majority of JavaScript for this project! All that’s left to do is pass these values to CSS-land. And we can do that in our callback. Let’s use custom properties named ratio-x and ratio-y.

const UPDATE = (x, y) => {   const clampedX = gsap.utils.clamp(-1, 1, x.toFixed(1))   const clampedY = gsap.utils.clamp(-1, 1, y.toFixed(1))   CONTAINER.style.setProperty('--ratio-x', clampedX)   CONTAINER.style.setProperty('--ratio-y', clampedY)   CONTAINER.innerText = `x: $ {clampedX}; y: $ {clampedY};` }  document.addEventListener('pointermove', generateHandler(CONTAINER, 100, UPDATE))

Now that we have some values we can use in our CSS, we can combine them with calc() any way we like. For example, this demo changes the scale of the container element based on the y value. It then updates the hue of the container based on the x value.

The neat thing here is that the JavaScript doesn’t care about what you do with the values. It’s done its part. That’s the magic of using scoped custom properties.

.container {   --hue: calc(180 - (var(--ratio-x, 0) * 180));   background: hsl(var(--hue, 25), 100%, 80%);   transform: scale(calc(2 - var(--ratio-y, 0))); }

Another interesting point is considering whether you want to clamp the values or not. In this demo, if we didn’t clamp x, we could have the hue update wherever we are on the page.

Making a scene

We have the technique in place! Now we can do pretty much whatever we want with it. It’s kinda wherever your imagination takes you. I’ve used this same set up for a bunch of things.

Our demos so far have only made changes to the containing element. But, as we may as well mention again, the power of custom property scope is epic.

My task was to make things move on Kent’s site. When I first saw the image of Kody with a bunch of objects, I could see all the individual pieces doing their own thing—all powered by those two custom properties that we pass in. How might that look though? The key is inline custom properties for each child of our container.

For now, we could update our markup to include some children:

<div class="container">   <div class="container__item"></div>   <div class="container__item"></div>   <div class="container__item"></div> </div>

Then we update the styles to include some scoped styles for container__item:

.container__item {   position: absolute;   top: calc(var(--y, 0) * 1%);   left: calc(var(--x, 0) * 1%);   height: calc(var(--size, 20) * 1px);   width: calc(var(--size, 20) * 1px);   background: hsl(var(--hue, 0), 80%, 80%);   transition: transform 0.1s;   transform:      translate(-50%, -50%)     translate(       calc(var(--move-x, 0) * var(--ratio-x, 0) * 100%),       calc(var(--move-y, 0) * var(--ratio-y, 0) * 100%)     )     rotate(calc(var(--rotate, 0) * var(--ratio-x, 0) * 1deg))   ; }

The important part there is how we’re making use of --ratio-x and --ratio-y inside the transform. Each item declares its own level of movement and rotation via --move-x, etc. Each item is also positioned with scoped custom properties, --x and --y.

That’s the key to these CSS powered parallax scenes. It’s all about bouncing coefficients against each other!

If we update our markup with some inline values for those properties, here’s what we get:

<div class="container">   <div class="container__item" style="--move-x: -1; --rotate: 90; --x: 10; --y: 60; --size: 30; --hue: 220;"></div>   <div class="container__item" style="--move-x: 1.6; --move-y: -2; --rotate: -45; --x: 75; --y: 20; --size: 50; --hue: 240;"></div>   <div class="container__item" style="--move-x: -3; --move-y: 1; --rotate: 360; --x: 75; --y: 80; --size: 40; --hue: 260;"></div> </div>

Leveraging that scope, we can get something like this! That’s pretty neat. It almost looks like a shield.

But, how do you take a static image and turn it into a responsive parallax scene? First, we’re going to have to create all those child elements and position them. And to do this we can use the “tracing” technique we use with CSS art.

This next demo shows the image we’re using inside a parallax container with children. To explain this part, we’ve created three children and given them a red background. The image is fixed with a reduced opacity and lines up with our parallax container.

Each parallax item gets created from a CONFIG object. For this demo, I’m using Pug to generate these in HTML for brevity. In the final project, I’m using React which we can show later. Using Pug here saves me writing out all the inline CSS custom properties individually.

-   const CONFIG = [     {       positionX: 50,       positionY: 55,       height: 59,       width: 55,     },     {       positionX: 74,       positionY: 15,       height: 17,       width: 17,     },     {       positionX: 12,       positionY: 51,       height: 24,       width: 19,     }   ]  img(src='https://assets.codepen.io/605876/kody-flying_blue.png') .parallax   - for (const ITEM of CONFIG)     .parallax__item(style=`--width: $ {ITEM.width}; --height: $ {ITEM.height}; --x: $ {ITEM.positionX}; --y: $ {ITEM.positionY};`)

How do we get those values? It’s a lot of trial and error and is definitely time consuming. To make it responsive, the positioning and sizing use percentage values.

.parallax {   height: 50vmin;   width: calc(50 * (484 / 479) * 1vmin); // Maintain aspect ratio where 'aspect-ratio' doesn't work to that scale.   background: hsla(180, 50%, 50%, 0.25);   position: relative; }  .parallax__item {   position: absolute;   left: calc(var(--x, 50) * 1%);   top: calc(var(--y, 50) * 1%);   height: calc(var(--height, auto) * 1%);   width: calc(var(--width, auto) * 1%);   background: hsla(0, 50%, 50%, 0.5);   transform: translate(-50%, -50%); }

Once we’ve made elements for all the items, we get something like the following demo. This uses the config object from the final work:

Don’t worry if things aren’t perfectly lined up. Everything is going to be moving anyway! That’s the joy of using a config object—we get tweak it how we like.

How do we get the image into those items? Well, it’s tempting to create separate images for each item. But, that would result in a lot of network requests for each image which is bad for performance. Instead, we can create an image sprite. In fact, that’s exactly what I did.

An image sprite of the original Kody image, showing each object and and Kody lined up from left to right.

Then to keep things responsive, we can use a percentage value for the background-size and background-position properties in the CSS. We make this part of the config and then inline those values, too. The config structure can be anything.

-   const ITEMS = [     {       identifier: 'kody-blue',       backgroundPositionX: 84.4,       backgroundPositionY: 50,       size: 739,       config: {         positionX: 50,         positionY: 54,         height: 58,         width: 55,       },     },   ]  .parallax   - for (const ITEM of ITEMS)     .parallax__item(style=`--pos-x: $ {ITEM.backgroundPositionX}; --pos-y: $ {ITEM.backgroundPositionY}; --size: $ {ITEM.size}; --width: $ {ITEM.config.width}; --height: $ {ITEM.config.height}; --x: $ {ITEM.config.positionX}; --y: $ {ITEM.config.positionY};`)

Updating our CSS to account for this:

.parallax__item {   position: absolute;   left: calc(var(--x, 50) * 1%);   top: calc(var(--y, 50) * 1%);   height: calc(var(--height, auto) * 1%);   width: calc(var(--width, auto) * 1%);   transform: translate(-50%, -50%);   background-image: url('kody-sprite.png');   background-position: calc(var(--pos-x, 0) * 1%) calc(var(--pos-y, 0) * 1%);   background-size: calc(var(--size, 0) * 1%); }

And now we have a responsive traced scene with parallax items!

All that’s left to do is remove the tracing image and the background colors, and apply transforms.

In the first version, I used the values in a different way. I had the handler return values between -60 and 60. We can do that with our handler by manipulating the return values.

const UPDATE = (x, y) => {   CONTAINER.style.setProperty(     '--ratio-x',     Math.floor(gsap.utils.clamp(-60, 60, x * 100))   )   CONTAINER.style.setProperty(     '--ratio-y',     Math.floor(gsap.utils.clamp(-60, 60, y * 100))   ) }

Then, each item can be configured for:

  • the x, y, and z positions,
  • movement on the x and y axis, and
  • rotation and translation on the x and y axis.

The CSS transforms are quite long. This is what they look like:

.parallax {   transform: rotateX(calc(((var(--rx, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1deg))     rotateY(calc(((var(--ry, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg))     rotate(calc(((var(--r, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg));   transform-style: preserve-3d;   transition: transform 0.25s; }  .parallax__item {   transform: translate(-50%, -50%)     translate3d(       calc(((var(--mx, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1%),       calc(((var(--my, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1%),       calc(var(--z, 0) * 1vmin)     )     rotateX(calc(((var(--rx, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1deg))     rotateY(calc(((var(--ry, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg))     rotate(calc(((var(--r, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg));   transform-style: preserve-3d;   transition: transform 0.25s; }

What’s that --allow-motion thing doing? That’s not in the demo! True. This is a little trick for applying reduced motion. If we have users who prefer “reduced” motion, we can cater for that with a coefficient. The word “reduced” doesn’t have to mean “none” after all!

@media (prefers-reduced-motion: reduce) {   .parallax {     --allow-motion: 0.1;   } } @media (hover: none) {   .parallax {     --allow-motion: 0;   } }

This “final” demo shows how the --allow-motion value affects the scene. Move the slider to see how you can reduce the motion.

This demo also shows off another feature: the ability to choose a “team” that changes Kody’s color. The neat part here is that all that requires is pointing to a different part of our image sprite.

And that’s it for creating a CSS custom property powered parallax! But, I did mention this was something I built in React. And yes, that last demo uses React. In fact, this worked quite well in a component-based environment. We have an array of configuration objects and we can pass them into a <Parallax> component as children along with any transform coefficients.

const Parallax = ({   config,   children, }: {   config: ParallaxConfig   children: React.ReactNode | React.ReactNode[] }) => {   const containerRef = React.useRef<HTMLDivElement>(null)   useParallax(     (x, y) => {       containerRef.current.style.setProperty(         '--range-x', Math.floor(gsap.utils.clamp(-60, 60, x * 100))       )       containerRef.current.style.setProperty(         '--range-y', Math.floor(gsap.utils.clamp(-60, 60, y * 100))       )     },     containerRef,     () => window.innerWidth * 0.5, )    return (     <div       ref={containerRef}       className='parallax'       style={         {           '--r': config.rotate,           '--rx': config.rotateX,           '--ry': config.rotateY,         } as ContainerCSS       }     >       {children}     </div>   ) } 

Then, if you spotted it, there’s a hook in there called useParallax. We pass a callback into this that receives the x and y value. We also pass in the proximity which can be a function, and the element to use.

const useParallax = (callback, elementRef, proximityArg = 100) => {   React.useEffect(() => {     if (!elementRef.current || !callback) return     const UPDATE = ({ x, y }) => {       const bounds = 100       const proximity = typeof proximityArg === 'function' ? proximityArg() : proximityArg       const elementBounds = elementRef.current.getBoundingClientRect()       const centerX = elementBounds.left + elementBounds.width / 2       const centerY = elementBounds.top + elementBounds.height / 2       const boundX = gsap.utils.mapRange(centerX - proximity, centerX + proximity, -bounds, bounds, x)       const boundY = gsap.utils.mapRange(centerY - proximity, centerY + proximity, -bounds, bounds, y)       callback(boundX / 100, boundY / 100)     }     window.addEventListener('pointermove', UPDATE)     return () => {       window.removeEventListener('pointermove', UPDATE)     }   }, [elementRef, callback]) }

Spinning this into a custom hook means I can reuse it elsewhere. In fact, removing the use of GSAP makes it a nice micro-package opportunity.

Lastly, the <ParallaxItem>. This is pretty straightforward. It’s a component that maps the props into inline CSS custom properties. In the project, I opted to map the background properties to a child of the ParallaxItem.

const ParallaxItem = ({   children,   config, }: {   config: ParallaxItemConfig   children: React.ReactNode | React.ReactNode[] }) => {   const params = {...DEFAULT_CONFIG, ...config}   return (     <div       className='parallax__item absolute'       style={         {           '--x': params.positionX,           '--y': params.positionY,           '--z': params.positionZ,           '--r': params.rotate,           '--rx': params.rotateX,           '--ry': params.rotateY,           '--mx': params.moveX,           '--my': params.moveY,           '--height': params.height,           '--width': params.width,         } as ItemCSS       }     >       {children}     </div>   ) }

Tie all that together and you could end up with something like this:

const ITEMS = [   {     identifier: 'kody-blue',     backgroundPositionX: 84.4,     backgroundPositionY: 50,     size: 739,     config: {       positionX: 50,       positionY: 54,       moveX: 0.15,       moveY: -0.25,       height: 58,       width: 55,       rotate: 0.01,     },   },   ...otherItems ]  const KodyParallax = () => (   <Parallax config={{     rotate: 0.01,     rotateX: 0.1,     rotateY: 0.25,   }}>     {ITEMS.map(item => (       <ParallaxItem key={item.identifier} config={item.config} />     ))}   </Parallax> )

Which gives us our parallax scene!

That’s it!

We just took a static image and turned it into a slick parallax scene powered by CSS custom properties! It’s funny because image sprites have been around a long time, but they still have a lot of use today!

Stay Awesome! ʕ •ᴥ•ʔ

The post Parallax Powered by CSS Custom Properties appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , ,

The Big Gotcha With Custom Properties

I’ve seen this confuse more than a handful of people recently, including myself, so I’m making sure it’s written down.

Let’s chuck a couple of custom properties into CSS:

html {   --color-1: red;   --color-2: blue; }

Let’s use them right away to make a background gradient:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); }

Now say there is a couple of divs sitting on the page:

<div></div> <div class="variation"></div>

Lemme style them up:

div {   background: var(--bg); }

That totally works! Hell yes!

Now lemme style that variation. I don’t want it to go from red to blue, I want it to go from green to blue. Easy cheesy, I’ll update red to green:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); } div {   background: var(--bg); } .variation {   --color-1: green; }

Nope! (Sirens blaring, horns honking, farm animals taking cover).

That doesn’t work, friends.

The problem, as best I understand it, is that --bg was never declared on either of the divs. It can use --bg, because it was declared higher up, but by the time it is being used there, the value of it is locked. Just because you change some other property that --bg happens to use at the time it was declared, it doesn’t mean that property goes out searching for places it was used and updating everything that’s used it as a dependency.

Ugh, that explanation doesn’t feel quite right. But it’s the best I got.

The solution? Well, there are a few.

Solution 1: Scope the variable to where you’re using it.

You could do this:

html {   --color-1: red;   --color-2: blue; }  div {   --bg: linear-gradient(to right, var(--color-1), var(--color-2));   background: var(--bg); } .variant {   --color-1: green; }

Now that --bg is declared on both divs, the change to the --color-1 dependency does work.

Solution 2: Comma-separate the selector where you set most of the variables.

Say you do the common thing where you set a bunch of variables at the :root. Then you run into this problem. You can just add extra selectors to that main declaration to make sure you hit the right scope.

html, div {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); } div {   background: var(--bg); } .variation {   --color-1: green; }

In some other perhaps less-contrived example, it might look something like this:

:root,  .button, .whatever-it-is-a-bandaid {   --padding-inline: 1rem;   --padding-block: 1rem;   --padding: var(--padding-block) var(--padding-inline); }  .button {   padding: var(--padding); } .button.less-wide {   --padding-inline: 0.5rem; }

Solution 3: Blanket Mode

Screw it — put the variables everywhere.

* {   --access: me;   --whereever: you;   --want: to;    --hogwild: var(--access) var(--whereever); }

This is not a good plan. I overheard a chat recently in which a medium-sized site experienced a 500ms page rendering delay because every draw to the page needed to compute all the properties. It “works” but it’s one of the rare cases where you can cause legit performance problems with a selector.

Solution 4: Introduce a new “default” property and fallback

All credit here to Stephen Shaw who’s exploration on all this is one of the places I saw this confusion in the first place.

Let’s go back to our first demonstration of this problem:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); }

What we want to do is give ourselves two things:

  1. A way to override the entire background
  2. A way to overide a part of the gradient background

So we’re gonna do it this way:

html {   --color-1: red;   --color-2: blue; } div {   --bg-default: linear-gradient(to right, var(--color-1), var(--color-2));   background: var(--bg, var(--bg-default)); }

Notice that we haven’t declared --bg at all. It’s just sitting there waiting for a value, and if it ever gets one, that’s the value that “wins.” But without one, it’ll fall back to our --bg-default. Now…

  1. If I set --color-1 or --color-2, it replaces that part of the gradient as expected (so long as I do it on a selector that touches one of the divs).
  2. Or, I can set --bg to reset the entire background to whatever I want.

Feels like a nice way to handle things.

Sometimes there are actual bugs with CSS custom properties. This isn’t one of them. Even though it sort of feels like a bug to me, apparently it’s not. Just one of those things you gotta know about.

The post The Big Gotcha With Custom Properties appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, ,