Tag: Properties

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.

CSS-Tricks

, , , ,

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.

Done!

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.

Conclusion

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.

CSS-Tricks

, , , , ,
[Top]

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.

Demo

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.

CSS-Tricks

, , , , ,
[Top]

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.

CSS-Tricks

, , , ,
[Top]

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.

CSS-Tricks

, , , ,
[Top]

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.

CSS-Tricks

, , , ,
[Top]

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.

CSS-Tricks

, , ,
[Top]

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.

CSS-Tricks

, ,
[Top]

Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc()

I recently wrote a very basic Sass loop that outputs several padding and margin utility classes. Nothing fancy, really, just a Sass map with 11 spacing values, looped over to create classes for both padding and margin on each side. As we’ll see, this works, but it ends up a pretty hefty amount of CSS. We’re going to refactor it to use CSS custom properties and make the system much more trim.

Here’s the original Sass implementation:

$  space-stops: (   '0': 0,   '1': 0.25rem,   '2': 0.5rem,   '3': 0.75rem,   '4': 1rem,   '5': 1.25rem,   '6': 1.5rem,   '7': 1.75rem,   '8': 2rem,   '9': 2.25rem,   '10': 2.5rem, );  @each $  key, $  val in $  space-stops {   .p-#{$  key} {     padding: #{$  val} !important;   }   .pt-#{$  key} {     padding-top: #{$  val} !important;   }   .pr-#{$  key} {     padding-right: #{$  val} !important;   }   .pb-#{$  key} {     padding-bottom: #{$  val} !important;   }   .pl-#{$  key} {     padding-left: #{$  val} !important;   }   .px-#{$  key} {     padding-right: #{$  val} !important;     padding-left: #{$  val} !important;   }   .py-#{$  key} {     padding-top: #{$  val} !important;     padding-bottom: #{$  val} !important;   }    .m-#{$  key} {     margin: #{$  val} !important;   }   .mt-#{$  key} {     margin-top: #{$  val} !important;   }   .mr-#{$  key} {     margin-right: #{$  val} !important;   }   .mb-#{$  key} {     margin-bottom: #{$  val} !important;   }   .ml-#{$  key} {     margin-left: #{$  val} !important;   }   .mx-#{$  key} {     margin-right: #{$  val} !important;     margin-left: #{$  val} !important;   }   .my-#{$  key} {     margin-top: #{$  val} !important;     margin-bottom: #{$  val} !important;   } } 

This very much works. It outputs all the utility classes we need. But, it can also get bloated quickly. In my case, they were about 8.6kb uncompressed and under 1kb compressed. (Brotli was 542 bytes, and gzip came in at 925 bytes.)

Since they are extremely repetitive, they compress well, but I still couldn’t shake the feeling that all these classes were overkill. Plus, I hadn’t even done any small/medium/large breakpoints which are fairly typical for these kinds of helper classes.

Here’s a contrived example of what the responsive version might look like with small/medium/large classes added. We’ll re-use the $ space-stops map defined previously and throw our repetitious code into a mixin

@mixin finite-spacing-utils($  bp: '') {     @each $  key, $  val in $  space-stops {         .p-#{$  key}#{$  bp} {             padding: #{$  val} !important;         }         .pt-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;         }         .pr-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;         }         .pb-#{$  key}#{$  bp} {             padding-bottom: #{$  val} !important;         }         .pl-#{$  key}#{$  bp} {             padding-left: #{$  val} !important;         }         .px-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;             padding-left: #{$  val} !important;         }         .py-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;             padding-bottom: #{$  val} !important;         }          .m-#{$  key}#{$  bp} {             margin: #{$  val} !important;         }         .mt-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;         }         .mr-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;         }         .mb-#{$  key}#{$  bp} {             margin-bottom: #{$  val} !important;         }         .ml-#{$  key}#{$  bp} {             margin-left: #{$  val} !important;         }         .mx-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;             margin-left: #{$  val} !important;         }         .my-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;             margin-bottom: #{$  val} !important;         }     } }  @include finite-spacing-utils;  @media (min-width: 544px) {     @include finite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include finite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include finite-spacing-utils($  bp: '_lg'); }  

That clocks in at about 41.7kb uncompressed (and about 1kb with Brotli, and 3kb with gzip). It still compresses well, but it’s a bit ridiculous.

I knew it was possible to reference data-* attributes from within CSS using the [attr() function, so I wondered if it was possible to use calc() and attr() together to create dynamically-calculated spacing utility helpers via data-* attributes — like data-m="1" or data-m="1@md" — then in the CSS to do something like margin: calc(attr(data-m) * 0.25rem) (assuming I’m using a spacing scale incrementing at 0.25rem intervals). That could be very powerful.

But the end of that story is: no, you (currently) can’t use attr() with any property except the content property. Bummer. But in searching for attr() and calc() information, I found this intriguing Stack Overflow comment by Simon Rigét that suggests setting a CSS variable directly within an inline style attribute. Aha!

So it’s possible to do something like <div style="--p: 4;"> then, in CSS:

:root {   --p: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } 

In the case of the style="--p: 4;" example, you’d effectively end up with padding: 1rem !important;.

… and now you have an infinitely scalable spacing utility class monstrosity helper.

Here’s what that might look like in CSS:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } [style*='--pt:'] {   padding-top: calc(0.25rem * var(--pt)) !important; } [style*='--pr:'] {   padding-right: calc(0.25rem * var(--pr)) !important; } [style*='--pb:'] {   padding-bottom: calc(0.25rem * var(--pb)) !important; } [style*='--pl:'] {   padding-left: calc(0.25rem * var(--pl)) !important; } [style*='--px:'] {   padding-right: calc(0.25rem * var(--px)) !important;   padding-left: calc(0.25rem * var(--px)) !important; } [style*='--py:'] {   padding-top: calc(0.25rem * var(--py)) !important;   padding-bottom: calc(0.25rem * var(--py)) !important; }  [style*='--m:'] {   margin: calc(0.25rem * var(--m)) !important; } [style*='--mt:'] {   margin-top: calc(0.25rem * var(--mt)) !important; } [style*='--mr:'] {   margin-right: calc(0.25rem * var(--mr)) !important; } [style*='--mb:'] {   margin-bottom: calc(0.25rem * var(--mb)) !important; } [style*='--ml:'] {   margin-left: calc(0.25rem * var(--ml)) !important; } [style*='--mx:'] {   margin-right: calc(0.25rem * var(--mx)) !important;   margin-left: calc(0.25rem * var(--mx)) !important; } [style*='--my:'] {   margin-top: calc(0.25rem * var(--my)) !important;   margin-bottom: calc(0.25rem * var(--my)) !important; }  

This is a lot like the first Sass loop above, but there’s no loop going 11 times — and yet it’s infinite. It’s about 1.4kb uncompressed, 226 bytes with Brotli, or 284 bytes gzipped.

If you wanted to extend this for breakpoints, the unfortunate news is that you can’t put the “@” character in CSS variable names (although emojis and other UTF-8 characters are strangely permitted). So you could probably set up variable names like p_sm or sm_p. You’d have to add some extra CSS variables and some media queries to handle all this, but it won’t blow up exponentially the way traditional CSS classnames created with a Sass for-loop do.

Here’s the equivalent responsive version. We’ll use a Sass mixin again to cut down the repetition:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  @mixin infinite-spacing-utils($  bp: '') {     [style*='--p#{$  bp}:'] {         padding: calc(0.25rem * var(--p)) !important;     }     [style*='--pt#{$  bp}:'] {         padding-top: calc(0.25rem * var(--pt)) !important;     }     [style*='--pr#{$  bp}:'] {         padding-right: calc(0.25rem * var(--pr)) !important;     }     [style*='--pb#{$  bp}:'] {         padding-bottom: calc(0.25rem * var(--pb)) !important;     }     [style*='--pl#{$  bp}:'] {         padding-left: calc(0.25rem * var(--pl)) !important;     }     [style*='--px#{$  bp}:'] {         padding-right: calc(0.25rem * var(--px)) !important;         padding-left: calc(0.25rem * var(--px)) !important;     }     [style*='--py#{$  bp}:'] {         padding-top: calc(0.25rem * var(--py)) !important;         padding-bottom: calc(0.25rem * var(--py)) !important;     }     [style*='--m#{$  bp}:'] {         margin: calc(0.25rem * var(--m)) !important;     }     [style*='--mt#{$  bp}:'] {         margin-top: calc(0.25rem * var(--mt)) !important;     }     [style*='--mr#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mr)) !important;     }     [style*='--mb#{$  bp}:'] {         margin-bottom: calc(0.25rem * var(--mb)) !important;     }     [style*='--ml#{$  bp}:'] {         margin-left: calc(0.25rem * var(--ml)) !important;     }     [style*='--mx#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mx)) !important;         margin-left: calc(0.25rem * var(--mx)) !important;     }     [style*='--my#{$  bp}:'] {         margin-top: calc(0.25rem * var(--my)) !important;         margin-bottom: calc(0.25rem * var(--my)) !important;     } }  @include infinite-spacing-utils;  @media (min-width: 544px) {     @include infinite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include infinite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include infinite-spacing-utils($  bp: '_lg'); }  

That’s about 6.1kb uncompressed, 428 bytes with Brotli, and 563 with gzip.

Do I think that writing HTML like <div style="--px:2; --my:4;"> is pleasing to the eye, or good developer ergonomics… no, not particularly. But could this approach be viable in situations where you (for some reason) need extremely minimal CSS, or perhaps no external CSS file at all? Yes, I sure do.

It’s worth pointing out here that CSS variables assigned in inline styles do not leak out. They’re scoped only to the current element and don’t change the value of the variable globally. Thank goodness! The one oddity I have found so far is that DevTools (at least in Chrome, Firefox, and Safari) do not report the styles using this technique in the “Computed” styles tab.

Also worth mentioning is that I’ve used good old padding  and margin properties with -top, -right, -bottom, and -left, but you could use the equivalent logical properties like padding-block and padding-inline. It’s even possible to shave off just a few more bytes by selectively mixing and matching logical properties with traditional properties. I managed to get it down to 400 bytes with Brotli and 521 with gzip this way.

Other use cases

This seems most appropriate for things that are on a (linear) incremental scale (which is why padding and margin seems like a good use case) but I could see this potentially working for widths and heights in grid systems (column numbers and/or widths). Maybe for typographic scales (but maybe not).

I’ve focused a lot on file size, but there may be some other uses here I’m not thinking of. Perhaps you wouldn’t write your code in this way, but a critical CSS tool could potentially refactor the code to use this approach.

Digging deeper

As I dug deeper, I found that Ahmad Shadeed blogged in 2019 about mixing calc() with CSS variable assignments within inline styles particularly for avatar sizes. Miriam Suzanne’s article on Smashing Magazine in 2019 didn’t use calc() but shared some amazing things you can do with variable assignments in inline styles.


The post Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , ,
[Top]

CSS Logical Properties and Values

Now that cross-browser support is at a tipping point, it’s a good time to take a look at logical properties and values. If you’re creating a website in multiple languages, logical properties and values are incredibly useful. Even if you’re not, there are still some convenient new shorthands it’s worth knowing about.

For example, I’ve lost count of the amount of times I’ve written this to center something:

.thing {   margin-left: auto;   margin-right: auto; }

We could make it a one-liner with something like margin: 0 auto; but then the top and bottom margins get thrown into the mix. Instead, we can select just the left and right margin with the margin-inline logical property.

Start thinking of things as “inline” or “block”

That last demo is pretty neat, right? The margin-inline property sets both margin-left and margin-right. Similarly, the margin-block property sets both margin-top and margin-bottom. And we’re not only talking margins. Logical properties has similar shorthands to set border and padding. So if you have a visual design that calls for borders only on the sides, you can just use border-inline instead of fussing with each physical direction on its own.

Showing border-left and border-right with matching values combined together as border-inline as a single declaration, and another example showing padding-top and padding-bottoms et to 32 pixels combined to padding-block set to 32 pixels.
Rather than thinking in physical terms, like left and right, we can think of an “inline” direction and a “block” direction.

So, as we move ahead, we now know that we’re dealing with inline and block directions instead of physical directions. Inline handles the left and right directions, while block manages top and bottom.

That is, until things get swapped around when the writing-mode changes.

Pay attention to direction and writing mode

What we’ve seen so far are examples of CSS logical properties. These are versions of CSS properties were used to like margin and padding, but written in a new way that forgoes physical directions (i.e. left, right, top, and bottom).

CSS was developed with the English language in mind and English is written and read from left-to-right. That’s not the case for all languages though. Arabic, for example, is read from right-to-left. That’s why HTML has a dir attribute.

<html dir="rtl">

CSS has an equivalent property (although it’s recommended to use the HTML attribute just in case the CSS fails to load):

.foreign-language { direction: rtl; }
Two cards, one in english and one in arabic, Both cards have a subtitle in gray above a main heading in a larger black font. The english goes from left to right and indicates the direction with an arrow below the card. The arabic direction is reverse of the english.
Credit: Ahmad Shadeed

Chinese, Japanese, Korean and Mongolian can be written either horizontally from left-to-right, or vertically from top to bottom. The majority of websites in these languages are written horizontally, the same as with English.

Comparatively, vertical writing is more common on Japanese websites. Some sites use a mixture of both vertical and horizontal text.

baroku.co.jp

When written vertically, Chinese, Japanese and Korean are written with the top-right as a starting point, whereas Mongolian reads from left to right. This is exactly why we have the writing-mode property in CSS, which includes the following values:

  • horizontal-tb: This is the default value, setting the the direction left-to-right for languages like English or French, and right-to-left languages like Arabic. The tb stands for “top-to-bottom.”
  • vertical-rl: This changes the direction to right-to-left in a vertical orientation for languages like Chinese, Japanese and Korean.
  • vertical-lr: This is used for vertical left-to-right languages, like Mongolian.

CSS logical properties offer a way to write CSS that is contextual. When using logical properties, spacing and layout are dependent on both the writing-mode and direction (whether set by either CSS or HTML). It therefore becomes possible to reuse CSS styles across different languages. BBC News, for example, rebuild their website in over a dozen languages. That’s a better experience than leaving users to rely on autotranslate. It also means they can better cater specific content to different parts of the world. The visual styling though, remains much the same across regions.

Screenshot of the BBC website. The header is red with the BBC logo aligned to the right of the screen. The navigation is also in red and aligned to the right. There is a featured article with right-aligned text and a large image to the right of it. Below that are four more article cards in a single row, each with an image above a title and date and aligned right.
bbc.com/arabic

Let’s look at the example below to see the shortcomings of physical properties. Using the physical margin-left property (shown in red), everything looks good in English. If you were to reuse the CSS but change the writing mode to rtl (shown at the bottom) there’s no space between the text and the icon and there’s excess white space on the left of the text. We can avoid this by using a logical property instead.

Two buttons, both with an envelope icon and a label. The left-to-right version of the button on top shows the spacing between the icon and the label. The right-to-left version shows the spacing to the left of both the label and icon.

What makes logical properties and values so useful is that they will automatically cater to the context of the language. In a left-to-right language like English, margin-inline-start will set the left-side margin. For a right-to-left language like Arabic, Urdu, or Hebrew, it will set the right-hand margin — which solves the layout problem in the above example. That’s right-to-left taken care of. If you have vertical text, margin-inline-start will cater to that context to, adding the margin at the top, which is where you would start reading from in any vertical language (that’s why it’s called margin-inline-start — just think about which direction you start reading from). The direction of inline changes based on the element’s writing-mode. When a vertical writing-mode is set, it handles the vertical direction top and bottom. See how things can get switched around?

An example of the writing direction in Mongolian. (Credit: W3C)

A complete list of logical properties and values

There are dozens of CSS properties that have a logical alternative syntax. Adrian Roselli has a handy visualization where you can toggle between the physical CSS properties that we’re all used to and their logical property equivalents. It’s a nice way to visualize logical properties and the physical properties they map to when the direction is ltr and the writing-mode is horizontal-tb.

Let’s break all of those down even further and map each and every physical CSS property to its logical companion, side-by-side. The tables shown throughout this article show traditional physical CSS in the left column and their logical equivalents (using a left-to-right horizontal mapping) in the right column. Remember though, the whole point of logical properties is that they change based on context!

Sizing

In a horizontal writing mode, inline-size sets the width of an element, while block-size sets the height. In a vertical writing mode, the opposite is true: inline-size sets the height and block-size sets the width.

Physical property Logical property
width inline-size
max-width max-inline-size
min-width min-inline-size
height block-size
max-height max-block-size
min-height min-block-size

Logical properties for sizing have good cross-browser support.

Borders

Everything here has solid cross-browser support among modern browsers.

Physical property Logical property
border-top border-block-start
border-bottom border-block-end
border-left border-inline-start
border-right border-inline-end

Here’s an example of using border-inline-start shown with English, Arabic, and Chinese.

Here’s an example that sets border-block-start dotted and border-block-end dashed:

There are also logical properties for setting the border color, width, and style individually:

Physical property Logical property
border-top-color border-block-start-color
border-top-width border-block-start-width
border-top-style border-block-start-style

So, again, it’s about thinking in terms of “inline” and “block” instead of physical directions, like left and top. We also have shorthand logical properties for borders:

Physical property Logical property
border-top and border-bottom border-block
border-left and border-right border-inline

Margin

Here are all the individual logical margin properties:

Physical property Logical property
margin-top margin-block-start
margin-bottom margin-block-end
margin-left margin-inline-start
margin-right margin-inline-end

These logical properties has comprehensive modern cross-browser support, including Samsung Internet, and has been supported in Safari since 12.2.

And, remember, we have the shorthands as well:

Physical property Logical property
margin-top and margin-bottom margin-block
margin-left and margin-right margin-inline

Padding

Padding is super similar to margin. Replace margin with padding and we’ve got the same list of properties.

Physical property Logical property
padding-top padding-block-start
padding-bottom padding-block-end
padding-left padding-inline-start
padding-right padding-inline-end
padding-top and padding-bottom padding-block
padding-left and padding-right padding-inline

Just like margins, logical properties for padding have good cross-browser support.

Positioning

Need to offset an element’s position in a certain direction? We can declare those logically, too.

Physical property Logical property
top inset-block-start
bottom inset-block-end
left inset-inline-start
right inset-inline-end
top and bottom inset-block
left and right inset-inline

In a horizontal writing mode (either left-to-right, or right-to-left) inset-block-start is equivalent to setting top, and inset-block-end is equivalent to setting bottom. In a horizontal writing mode, with a left-to-right direction, inset-inline-start is equivalent to left, while inset-inline-end is equivalent to right, and vice-versa for right-to-left languages.

Conversely, for a vertical writing mode, inset-inline-start is equivalent to top while inset-inline-end is equivalent to bottom. If writing-mode is set to vertical-rl, inset-block-start is equivalent to right and inset-block-end is equivalent to left. If the writing-mode is set to vertical-lr, the opposite is the case and so inset-block-start is equivalent to left.

Logical property Writing mode Equivalent to:
inset-block-start` Horizontal LTR top
inset-block-start Horizontal RTL top
inset-block-start Vertical LTR left
inset-block-start Vertical RTL right

Here’s an example of how the same CSS code for absolute positioning looks in each of the four different writing directions:

Logical properties for positioning are supported in all modern browsers, but only recently landed in Safari.

There’s also a new shorthand for setting all four offsets in one line of code. Here’s an example using inset as a shorthand for setting top, bottom, left, and right in one fell swoop to create a full-page overlay:

I’ve heard inset incorrectly referred to as a logical property. But, a quick look in DevTools shows that it is actually a shorthand for physical values, not logical properties:

What it’s actually doing is defining physical offsets (i.e. left, right, top and bottom) rather than logical ones (i.e. inline, block, start and end). Obviously if you want to set the same value for all four sides, as in the example above, it doesn’t matter.

inset: 10px 20px 5px 8px; /* shorthand for physical properties not logical properties  */

Text alignment

Logical values for text alignment enjoy great browser support and have for many years. When working in English, text-align: start is the same as text-align: left, while text-align: end is the same as text-align-right. If you set the dir attribute to rtl, they switch and text-align: start aligns text to the right.

Physical value Writing mode Equivalent to:
start LTR left
start RTL right
end LTR right
end RTL left

Border radius

So far everything we’ve looked at has decent browser support. However, there are some other logical properties where support is still a work in progress, and border radius is one of them. In other words, we can set a different border-radius value for different corners of an element using logical properties, but browser support isn’t great.

Physical property Logical property
border-top-left-radius border-start-start-radius
border-top-right-radius border-start-end-radius
border-bottom-left-radius border-end-start-radius
border-bottom-right-radius border-end-end-radius

It’s worth noting that the spec doesn’t include shorthand properties, like border-start-radius and border-end-radius. But, like I said, we’re still in early days here, so that might be a space to watch.

Floats

Flow-relative values for logical floats have terrible browser support at the time I’m writing this. Only Firefox supports inline-start and inline-end as float values.

Physical value Logical value
float: left float: inline-start
float: right float: inline-end
clear: left clear: inline-start
clear: right clear: inline-end

Other logical properties

There are proposed logical properties for overflow and resize, but they currently have horrendous browser support.

Physical Logical
resize: vertical resize: block
resize: horizontal resize: inline
overflow-y overflow-block
overflow-x overflow-inline

Digging deeper

We explored what it means for a property to be considered “logical” and then mapped out all of the new logical properties and values to their physical counterparts. That’s great! But if you want to go even deeper into CSS Logical Properties and Values, there are a number of resources worth checking out.

  • “RTL Styling 101” (Ahmad Shadeed): A great resource if you’re dealing with Arabic or other right-to-left languages. Ahmad covers everything, from logical properties to considerations when working with specific layout techniques, like flexbox and grid.
  • text-combine-upright (CSS-Tricks): If you’re dealing with vertical text, did you know that this property can rotate text and squeeze multiple characters into the space of a single character? It’s a nice touch of refinement in specific situations where some characters need to go together but still flow with a vertical writing mode.

If you want to view some nice real-world examples of vertical typography from across the web, take a look at the Web Awards for Horizontal and Vertical Writings. There’s a lot of great stuff in there.

Wrapping up

Do you need to rush and swap all of the physical properties out of your codebase? Nope. But it also doesn’t hurt to start using logical properties and values in your work. As we’ve seen, browser support is pretty much there. And even if you’re working on a site that’s just in English, there’s no reason to not use them.


The post CSS Logical Properties and Values appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]