Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 21 Aug 2025 20:50:42 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 You really don’t have to put your CSS custom properties in :root {} https://frontendmasters.com/blog/you-really-dont-have-to-put-your-css-custom-properties-in-root/ https://frontendmasters.com/blog/you-really-dont-have-to-put-your-css-custom-properties-in-root/#comments Thu, 21 Aug 2025 20:50:41 +0000 https://frontendmasters.com/blog/?p=6836 I feel like most usage of global CSS custom property use has the author putting them into a :root selector like:

:root {
  --brand-color: red;
}

This is just a PSA that you… don’t have to. It’s not required. It has nothing to do with custom properties. It’s just a selector. It happens to select the html element on a website, except for with extra specificity that I’d wager isn’t typically useful. I think the earliest code samples of them in the spec did it that way for whatever reason, and it stuck as a weird niche cultural artifact.

Well (you shake your dirty martini at me) Chris — I just prefer :root because not all CSS usage is within an HTML website. You can put a <style> tag in <svg> you know!!!

Yes, and if you do that, :root will still select the html element, not the “root” of the <svg> element. So that’s weird. It’s true that if you do SVG-as-<img> it will select the root of the SVG, but that difference in behavior is probably reason enough not to do it.

Anyway — just quit being fancy and use the html element.

]]>
https://frontendmasters.com/blog/you-really-dont-have-to-put-your-css-custom-properties-in-root/feed/ 3 6836
Little Reminder About Custom Properties with Invalid Values https://frontendmasters.com/blog/little-reminder-about-custom-properties-with-invalid-values/ https://frontendmasters.com/blog/little-reminder-about-custom-properties-with-invalid-values/#respond Tue, 05 Aug 2025 19:33:43 +0000 https://frontendmasters.com/blog/?p=6703 element sitting there in a totally normal basic HTML layout, then this CSS: What color does the <p> render as? It’s blue. You might think it’s green, as the value blah is an invalid color. If the CSS had… […]]]> This is like one of those weirdly difficult quizzes about CSS. If you’ve got a <p> element sitting there in a totally normal basic HTML layout, then this CSS:

html {
  --color: blah;
}
body {
  color: blue;
}
p {
  color: green;
  color: var(--color);
}

What color does the <p> render as?

It’s blue. You might think it’s green, as the value blah is an invalid color. If the CSS had…

p {
  color: green;
  color: blah;
}

… then indeed it would be green. But CSS can’t know (in time) that the custom property is invalid, so it falls back to inheritance, which makes it blue from the body.

]]>
https://frontendmasters.com/blog/little-reminder-about-custom-properties-with-invalid-values/feed/ 0 6703
CSS Spotlight Effect https://frontendmasters.com/blog/css-spotlight-effect/ https://frontendmasters.com/blog/css-spotlight-effect/#comments Mon, 26 May 2025 15:02:35 +0000 https://frontendmasters.com/blog/?p=5939 I recently made an experiment about Proximity Reactions. The idea was to create an interactive effect according to the mouse position relative to elements. Then I made a less JavaScript, more CSS version where the only thing JavaScript does is to pass the mouse position into a couple of CSS custom properties. That’s it. All the heavy lifting happened inside the CSS itself, safely away from the JavaScript thread.

That got me thinking: if we can get the mouse position in CSS so easily, what else can we build with that? I started tinkering, trying out different interaction patterns, and eventually got to this Spotlight Effect that’s easy to create, simple to customize, and looks surprisingly slick, all with just a few lines of CSS.

Let’s take a look at how it works and how you can make it your own, and hopefully you can pick up a few new CSS tricks along the way. 🙂

The Setup

To create a spotlight effect that responds to the mouse position, we need to set up two small things before diving into the CSS.

  1. We need a dedicated spotlight element in the DOM. This is usually placed near the end of the markup so it can sit on top of everything else when needed.
  2. We need just a few lines of JavaScript to pass the mouse coordinates into CSS custom properties.
<div class="spotlight"></div>
document.body.addEventListener('mousemove', (e) => {
  document.body.style.setProperty('--clientX', e.clientX + 'px');
  document.body.style.setProperty('--clientY', e.clientY + 'px');
});

That is all. No fancy libraries, no event throttling, just raw coordinates handed over to CSS, where the real magic happens.

Basic follow

Now that the setup is in place, we can start writing some CSS. We will begin with a very basic version of the spotlight effect: a simple transparent circle that follows the mouse movements. There are many ways to implement this kind of effect. Using transform is a common and often more precise approach in some cases. But for our example, we are going to tap into the power of background-image. This gives us a lot of creative flexibility, especially when we’ll start creating patterns with gradients later on.

Here is the CSS for our initial spotlight:

.spotlight {
  position: fixed;
  inset: 0;
  background-image: radial-gradient(circle at var(--clientX, 50%) var(--clientY, 50%), transparent 6em, black 8em);
}

Notice that we set position: fixed and inset: 0, this ensures that it fills the entire viewport, anchoring it to the edges of the body, and stays in place when the user scroll down the page. With that in place, we can position the transparent circle (made with a simple radial-gradient) using the CSS custom properties that our JavaScript sets. It really is that simple.

I’m using em units for sizing. This makes everything scale relative to the font size, and it makes it very easy to adjust the size of the entire effect just by changing the font size on this element.

Here is the result:

To make the effect feel a bit lighter, I also added a touch of opacity. I think it creates a more layered and subtle look. More importantly, I set pointer-events: none on the .spotlight element. Since this layer sits above everything else in the DOM, we want to make sure it does not block any user interaction with the elements below it. Without this, buttons, links, and other interactive parts of the page would become unresponsive.

I’m not using cursor: none; here. While it might seem like an good choice for effects like this, hiding the mouse cursor can lead to accessibility issues and negatively impact the user experience. It’s generally best to avoid it.

Making It Interesting

This is where things start to get fun. Instead of a simple circle, we can turn our spotlight into a dynamic, interactive effect that responds to the mouse movement in playful ways. The technique we will use involves layering gradients in the background-image and combining them in a gooey visual style. The result is a smooth, organic animation that feels alive under the cursor.

To achieve the gooey effect, we rely on the filter property, specifically a combination of blur and contrast. The blur softens the edges of the shapes, and the high contrast causes overlapping areas to merge into blobs. However, applying contrast on a transparent background does nothing. To fix that, we give the element a solid white background-color. Then, to make the white areas effectively transparent against the page, we use mix-blend-mode: darken.

1) Start with a basic spotlight

Add a pattern using the background-image

Set the background-color to white

Apply the filter for the gooey effect

Remove the white parts using mix-blend-mode

And here is the code that sets up this visual base:

.spotlight {
  filter: blur(1em) contrast(100);
  mix-blend-mode: darken;
  background-color: white;
}

Now that we have this setup, we can start layering more shapes, play with gradients, and watch the gooey interactions evolve as the mouse moves.

The Blob Light

With the gooey base in place, we can use gradients to build more playful visual behaviors. Since background-image can accept a comma-separated list of layers, we can stack several gradients with varying styles, sizes, and positions. These layers blend together through the blur and contrast filters, resulting in a smooth, organic effect.

To create a blob-style spotlight, I made the main circle a bit larger and softer, and stacked two repeating linear gradients to form a diagonal grid pattern.

.spotlight {
  background-image:
    radial-gradient(circle at var(--clientX, 50%) var(--clientY, 50%), transparent, black 20em),
    repeating-linear-gradient(45deg, black 0 0.4em, transparent 0 3em),
    repeating-linear-gradient(-45deg, black 0 0.4em, transparent 0 3em);
}

This is how the background-image looks like without the gooey setup:

And the full blob effect:

Fixing the Fuzzy Edges

You may have noticed in the previous example that the edges of the .spotlight element appear fuzzy, subtly revealing the content behind it. This is a side effect of the blur filter. When there’s nothing beyond the blurred edge for the contrast filter to respond to, the gradient just fades out softly. Visually, that results in blurry borders that break the clean feel of the effect.

There are a few ways to deal with this. Like scaling up the element, applying a negative inset, or manually setting a larger width and height. But all of these approaches introduce extra complexity, especially since you’d also have to compensate for the mouse coordinates shifting relative to the larger area.

A simpler and more robust fix is to add an outline. Just make sure it’s larger than the blur radius and matches the background color. That way, the fuzzy edges get hidden cleanly without affecting the positioning logic at all.

.spotlight {
  outline: 2em solid white;
}

We’ll include this outline fix in all the following examples to keep things clean and crisp.

Dotted Reveal

The reason the blob in the previous example appears to morph as the mouse moves is that, while the main circle follows the cursor, the grid pattern remains fixed on the screen. The interaction between these two layers creates the illusion of motion and shifting shapes within the spotlight.

Following the same principle, we can build a dotted effect. This time, instead of diagonal lines, we’ll use two radial gradients, and set a background-size to create a repeating pattern:

.spotlight {
  background-image:
    radial-gradient(circle at var(--clientX, 50%) var(--clientY, 50%), transparent 6em, black 10em),
    radial-gradient(circle, black 0.2em, transparent 1em),
    radial-gradient(circle, black 0.2em, transparent 1em);
  background-size: 100% 100%, 2em 3em, 2em 3em;
  background-position: 0 0, 0 0, 1em 1.5em;
}

The first layer defines the moving mask (just like before), and the next two layers form the repeating dot pattern. By adjusting background-position, we offset the second dot layer to create the alternating effect. The result is a playful dotted texture that dynamically follows the mouse.

All of the values in the last two examples (color stops, gradient sizes and positions, blur and contrast settings, and more) can be tweaked to create wildly different effects. I spent a lot of time experimenting before landing on these particular numbers, and I encourage you to do the same. Go ahead and fork one of the demos, adjust the gradients, play with the filter values, and see where your creativity takes you. And if you discover something cool, don’t forget to send it my way.

Movement Interaction

In the previous examples, only the main circle responded to the cursor movement, but those same CSS variables can drive other visual elements as well. Here is an example that lays out a grid of squares using a conic-gradient. By offsetting its position by a fraction of the cursor coordinates (a factor of negative 0.25 in this case) we achieve a subtle parallax effect.

.spotlight {
  background-image:
    radial-gradient(circle at var(--clientX, 50%) var(--clientY, 50%), transparent, black 14em),
    conic-gradient(from 270deg at 1em 1em, #aaa 90deg, transparent 0);
  background-size: 100% 100%, 3em 3em;
  background-position: 
    0 0, 
    calc(var(--clientX, 50%) * -0.25) calc(var(--clientY, 50%) * -0.25); /* only the conic layer moves */
}

You can comment out the background-position to see its affect, and feel free to tweak the offset factor and see how the motion transforms.

Tip: try adding a transition on the background’s position to add even more motion. .spotlight { transition: background-position 0.5s ease-out; }

Remember these values can be used for anything. They’re just variables, and that means you can plug them into any CSS property that accepts dynamic values. For example, here’s an example where the mouse’s X position controls the amount of blur, and the Y position determines the size of the central circle.

With just two custom properties, you’re suddenly controlling not only movement, but also style and intensity. You could just as easily hook the mouse into opacity, gradient angles, or any part of the effect you want to feel dynamic. What would you change in your effect?

The Full Reveal

Up until now, we’ve been revealing only what’s inside the spotlight, with everything else hidden behind the dark blur. But what if we want to fully reveal the page in certain cases? For example, when hovering over a specific element, we might want to turn the effect off entirely and let the full content show.

Surprisingly, you don’t need any JavaScript to do this. With one clever CSS selector, we can ‘listen’ for a hover on elements with a specific class and adjust the effect accordingly.

.spotlight {
  transition: opacity 1s, background-color 1s;

  body:has(.reveal:hover) & {
    opacity: 0;
    background-color: black;
  }
}

Now, any element with class="reveal" will temporarily disable the spotlight effect when hovered.

In terms of styling, there are a few ways to disable the effect. You could scale the gradient out, reduce the blur, or even hide the entire .spotlight element. In this case, I went with a combination of lowering the opacity and changing the background color. This gave me a subtle fade effect both in and out.

The Light Spotlight

Until now, the hidden part of the page has been covered in black, creating a dark spotlight effect. But what if your design calls for a light version, with white as the cover color?

Turns out it’s pretty straightforward. All we need to do is invert the colors in our .spotlight element’s styles. Anything that was black becomes white, anything that was white becomes black (transparent stays as-is). And just as important, make sure to change the mix-blend-mode from darken to lighten so that the blending works correctly with the inverted color scheme.

Of course, these values don’t have to be hard coded. You can define the colors and blend mode using CSS custom properties, giving you full control over the theme. Better yet, we can respond to user preferences using the light-dark() function and the prefers-color-scheme query to decide whether to use a light or dark spotlight effect.

:root {
  color-scheme: light dark;

  --spotlight-cover: light-dark(white, black);
  --spotlight-reveal: light-dark(black, white);

  @media (prefers-color-scheme: dark) {
    --spotlight-blend-mode: darken;
  }
  
  @media (prefers-color-scheme: light) {
    --spotlight-blend-mode: lighten;
  }
}

This approach not only makes your spotlight more flexible, but also keeps it aligned with accessibility and user experience best practices.

Adding Colors

So what about colors beyond just black or white? Black and white are great for creating strong contrast, but what if you want something a bit more… purple?

Well, at this point, we need to slightly rethink our approach. The gooey technique we’ve used so far works beautifully with monochrome because of the way mix-blend-mode interacts with light and dark. As soon as you start introducing color, things get trickier. The blend mode can dramatically shift the look and feel depending on how your chosen colors interact with the background and with each other.

You can try changing the colors to something like purple or teal, but it will alter the nature of the effect, sometimes in surprising ways, so I encourage you to experiment. And how knows, you might land on exactly the vibe you’re looking for.

Mobile Support

This entire effect relies on mouse movement, so what happens when there’s no mouse? Rather than hiding content on touch devices, we’ll simply disable the effect altogether when we detect a mobile or touch-based screen. That way, users still see everything, just without the fancy spotlight interaction.

We can ensures that a device support hover interactions using the hover media query, which is supported on all major browsers. By wrapping the spotlight styles in a @media (hover: hover) we can apply the effect only on hover supported devices.

@media (hover: hover) {
  .spotlight {
    /* spotlight styles */
  }
}

This media query works well for most cases, but some devices support both touch and mouse input. Think touchscreen laptops or tablets with external mice. In those cases, the effect might kick in when it shouldn’t.

To handle this more gracefully, we can back up our CSS with a small JavaScript snippet. It listens for a touch event and disables the effect as soon as a user interacts via touch. That way, the spotlight effect is removed dynamically if the device leans toward touch input.

const mouseMoveHandler = (e) => {
  document.body.style.setProperty('--clientX', e.clientX + 'px');
  document.body.style.setProperty('--clientY', e.clientY + 'px');
};

document.body.addEventListener('mousemove', mouseMoveHandler);

document.body.addEventListener('touchstart', () => {
  document.body.classList.add('reveal');
  document.body.removeEventListener('mousemove', mouseMoveHandler);
});

And one last thing on this topic: we should also account for basic keyboard navigation. We do not want users tabbing into elements that are visually hidden by the effect, so we will also disable it in that case. This can be detected using body:has(:focus-visible), which tells us when one of our elements is focused. You can combine this selector with your .reveal logic to ensure the effect is turned off when keyboard navigation kicks in.

@media (hover: hover) {
  .spotlight {
    /* spotlight styles */

    body:has(.reveal:hover, :focus-visible) & {
      opacity: 0;
      background-color: black;
    }
  }
}

With this setup, the effect behaves just right: it kicks in only when it makes sense and stays out of the way when it doesn’t. Mobile users still get the full content, and hybrid devices adapt in real time.

The Ultimate Spot

Before we wrap up, here is a quick demo that brings together most of what we explored. A spotlight with a blob gooey effect, crisp edges, theme switching, and full mobile and keyboard navigation support. All within scrollable content, with areas that disable the effect on hover.

Taking It Further

All of the ideas in this article are just starting points. Now it’s your turn to run with them. You can play with gradient backgrounds and tweak their sizes and positions. You can experiment with filter settings or try different blend mode options to see what new moods emerge. You might also pull extra data from JavaScript (like the cursor angle relative to an element or the speed of movement) and feed that into your styles for even richer effects.

In this article, I’ve used a single <div> for the .spotlight element, but feel free to layer in additional elements, icons, text, or graphic shapes within the reveal area. Apply the same technique to multiple elements with their own custom settings. The possibilities are endless, so let your imagination guide you and discover what unique interactions you can build.

]]>
https://frontendmasters.com/blog/css-spotlight-effect/feed/ 8 5939
Using Container Query Units Relative to an Outer Container https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/ https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/#respond Tue, 06 May 2025 23:53:28 +0000 https://frontendmasters.com/blog/?p=5761 Recently, Matt Wilcox posted on Mastodon:

The fact you can’t specify which container for container query units is a ballache. The moment you have nested containers you’re [screwed]; because if you want the calculated gap from the row’s container; but you’re inside a nested container… tough. Your units are wrong. And you can’t just say “no; not relative to this container; relative to the named outer container!”

First off, if you’re not familiar with container queries and container query units, you can check out one of the many resources on the topic, for example this interactive guide by Ahmad Shadeed, which I believe is the most recent out of all the detailed ones I’ve seen. As always, the date of the resources used is important for web stuff, especially since these units in particular have changed their name since they were first proposed and we got an early draft of the spec.

Now, the problem at hand: let’s say we have an .inner-container inside an .outer-container – they are both made to be containers:

[class*='container'] { container-type: size }

We want any .inner-child of the .inner-container to be able to use length values set in container query units relative to the .outer-container (more precisely, to its content-box dimensions). The problem is, if we do something like this (a 20cqw light blue strip at the start of the gradient going towards 3 o’clock):

.inner-child {
  background: linear-gradient(90deg, #0a9396 20cqw, #0000)
}

… then the 20cqw value is 20% (a fifth) of the content-box width of the .inner-container. This can be seen below, where we have purple guidelines 20% of the width apart.

Screenshot illustrating how a background sized to cqw on the child of the inner container is a fifth of the inner container's width.
what 20cqw represents

But what we want is for that 20cqw value to be 20% of the content-box width of the .outer-container.

Strictly for the queries themselves, we could do something like this:

.outer-container { container: outer/ size }
.inner-container { container: inner/ size }

@container outer (min-width: 500px) {
  .inner-child { background: darkorange }
}

This allows us to set certain styles on the .inner-child elements based on where the width of the .outer-container (which isn’t the nearest container for .inner-child) is situated relative to the 500px threshold.

But we cannot do something like this to specify which container should be the one that the query units used on .inner-child are relative to:

.inner-child {
  /* can't do this */
  background: linear-gradient(90deg, #0a9396 outer 20cqw, #0000)
}

Nor can we do this:

.inner-child {
  /* can't do this either */
  --s: outer 20cqw;
  background: linear-gradient(90deg, #0a9396 var(--s), #0000)
}

However, we are getting closer!

What if we move the --s variable uspstream? After all, a 20cqw length value set on the .inner-container is 20% of the content-box width of its nearest container, which is the .outer-container. This would mean our code becomes:

[class*='container'] { container-type: size }

.inner-container {
  --s: 20cqw;
  background: 
    repeating-linear-gradient(45deg, #bb3e03 0 5px, #0000 0 1em) 
      0/ var(--s) no-repeat
}

.inner-child {
  background: 
    linear-gradient(90deg, #0a9396cc var(--s), #0000)
}

We also give the .inner-container a similar background restricted to 20cqw from the left along the x axis and make the .inner-child semi-transparent, just to check if the --s values overlap (which is what we want, --s being 20% or a fifth of the .outer-container width). However, this fails, as it can be seen below:

Screenshot. Both the inner container and its child have a background sized to 20cqw. However, the container query units are relative to the outer container only for the inner container, the container query units used on its child being still relative to the inner container (one fifth of its content-box width).
screenshot of result

For the .inner-container the 20cqw of the --s is taken to be 20% of the content-box width of its nearest container, .outer-container (dashed dark blue boundary). However, for the .inner-child, the 20cqw of the --s aren’t taken to mean the same value. Instead, they are taken to mean 20% of the .content-box width of the .inner-container (dotted dark red boundary).

Boo!

But what happens if we also register --s?

@property --s {
  syntax: '<length>';
  initial-value: 0px;
  inherits: true
}

Bingo, this works!

Screenshot. Both the inner container and its child have a background sized to 20cqw, the container query units being relative to the outer container.
desired result

I hope you’ve enjoyed this little trick.

Where would you use this?

]]>
https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/feed/ 0 5761
Sharing a Variable Across HTML, CSS, and JavaScript https://frontendmasters.com/blog/sharing-a-variable-across-html-css-and-javascript/ https://frontendmasters.com/blog/sharing-a-variable-across-html-css-and-javascript/#respond Wed, 08 Jan 2025 15:22:38 +0000 https://frontendmasters.com/blog/?p=4908 My kid is in a little phase where word search puzzles are really fun. She likes doing them, and then possibly because we share blood, she immediately started to want to make them. I figured it would be a fun little recreational coding job to build a maker, so I did that: Word Search Puzzle Maker. It’s not fancy, you just write in your words then a button click will fill in the un-filled spaces with random letters and you’re good to print it out.

One of the configuration options for the “maker” is how big you want to build the grid. A 10×10 grid is the default, but it’s settable by just setting a variable in one place.

It turns out it’s useful to have this variable in all three of the important front-end technologies at play: the HTML, CSS, and JavaScript. The relevant variable here is size, which represents the number of cells across and tall the grid is.

  • HTML — Well, Pug, actually. Pug generates HTML, and having the size there means I can write a loop that generates the number of elements in the grid the way I need.
  • CSS — Having the size there means I can set up the CSS grid with the appropriate columns/rows.
  • JavaScript — By having the size variable available there, I was able to implement arrow key navigation fairly easily which helped the experience of setting new words.

It all starts with that Pug code, so, ya know, sorry if that’s cheating. But here’s the rub:

- const size = 10;
script 
  | window.size = #{size};
  | document.documentElement.style.setProperty('--size', #{size});

The dash (-) in that Pug code essentially means “this is JavaScript”, and the Node process that runs to create the HTML runs it. That means I can use the variable later to create the grid.

.grid
  - for (let i = 0; i < size**2; i++)
    .letter(data-index=i)
      input(maxlength=1, matches="[A-Za-z]")

The variable gets passed from Pug into client-side JavaScript by making a script tag and creating a variable off the window object. A little variable interpolation makes that possible.

The variable gets passed to CSS in a similar fashion, using client-side JavaScript to call setProperty on the documentElement. That CSS custom property will then cascade to wherever we need it. I can use it on another element like this:

.grid {
  display: grid;
  grid-template-columns: repeat(var(--size), 1fr);
}

That’s it really. I just got a kick out of setting a variable in one place and making use of it in three languages.

Try changing the size above.

]]>
https://frontendmasters.com/blog/sharing-a-variable-across-html-css-and-javascript/feed/ 0 4908
CSS Fan Out with Grid and @property https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/ https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/#comments Wed, 09 Oct 2024 13:27:37 +0000 https://frontendmasters.com/blog/?p=4128 A “fan out” is an expanding animation where a group of items appear one after another, next to each other, as though they were spread out from a stack. There’s usually a subtle bounciness in the reveal.

The effect is customarily achieved by timing and positioning each of the items individually with very specific hard set values. That can be an awful lot of work though. We can make things a bit easier if we let the items’ parent container do this for us. Here’s a result of doing it this way: 

UPDATE: This article has been updated to now include the animation of the grid items’ height, to produce an overall smoother transition effect. The previous version of this article didn’t cover that. 

For HTML, there’s a group of items (plus an empty one — I will explain later why it’s there), bookended by two radio controls to prompt the opening and closing of the items respectively.

<section class="items-container">
  <p class="items"><!--empty--></p>
  <label class="items close">
    Close the messages<input type="radio" name="radio">
  </label>
  <p class="items">Alert from Project X</p>
  <p class="items">&#x1F429; Willow's appointment at <i>Scrubby's</i></p>
  <p class="items">Message from (-_-)</p>
  <p class="items">NYT Feed: <u>Weather In... (Read more)</u></p>
  <p class="items">6 more items to check in your vacation list!</p>
  <label class="items open">
    Show the messages<input type="radio" name="radio">
  </label>
</section>

We need a grid container for this to work, so let’s turn the <section>, the items’ container element, into one. You could use a list or anything you feel is semantically appropriate.

.items-container {
  display: grid; 
}

Now create an Integer CSS custom property with a value same as the number of items inside the container (including the open and close controls, and the empty item). This is key to implement the revealing and hiding of the items, sequentially, from within the grid container’s style rule.

Also, register another CSS custom property of data type length that’ll be used to animate each item’s height during the opening and closing of the control, for a smoother execution of the overall action. 

@property --int {
  syntax: "<integer>";
  inherits: false;
  initial-value: 7;
}

@property --hgt {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

Use the now created --int and --hgt properties to add that many grid rows of zero height in the grid container. 

.items-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), var(--hgt));  
}

When directly adding --int to repeat() it was producing a blotchy animation in Safari for me, so I fed it through calc() and the animation executed well (we’ll look into the animation in a moment). However, calc() computation kept leaving out one item in the iteration, because of how it computed the value 0. Hence, I added the empty item to compensate the exclusion. 

If Safari did not give me a blotchy result, I would’ve not needed an empty item, --int’s initial-value would’ve been 6, and grid-template-rows’s value would’ve been just repeat(var(--int), 0px). In fact, with this set up, I got good animation results both in Firefox and Chrome. 

In the end though, I went with the one that uses calc(), which provided the desired result in all the major browsers. 

Let’s get to animation now:

@keyframes open { to { --int: 0; --hgt:60px;} }
@keyframes close { to { --int: 6; --hgt:0px;} } 
.item-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), var(--hgt)); 
  &:has(.open :checked) {
  /* open action */
    animation: open .3s ease-in-out forwards;
    .open { display: none; }
  }
  &:has(.close :checked) {
  /* close action */
    --int: 0;
    --hgt: 60px;
    animation: close .3s ease-in-out forwards;
  }
}

When the input is in the checked state, the open keyframe animation is executed, and the control itself is hidden with display: none

The open class changes --int’s value from its initial-value, 7, to the one set within the @keyframes rule (0), over a set period (.3s). This decrement removes the zero height from each of the grid row, one by one, thus sequentially revealing all the items in .3s or 300ms. Simultaneously, --hgt’s value is increased to 60px from its initial 0px value. This expands each item’s height as it appears on the screen. 

When the input to hide all the items is in the checked state, the close keyframe animation is executed, setting --int’s value to 0 and --hgt’s value to 60px.

The close class changes the now 0 value of --int to the value declared in its rule: 7. This increment sets a zero height to each of the grid row, one by one, thus sequentially hiding all the items. Simultaneously, --hgt’s value is decreased to 0px. This shrinks each item’s height as it disappears from the screen. 

To perform the close action, instead of making a unique close animation, I tried using the open animation with animation-direction: reverse. Unfortunately, the result was jerky. So I kept unique animations for the open and close actions separately.

Additionally, to polish the UI, I’m adding transition animations to the row gaps and text colors as well. The row gaps set cubic-bezier() animation timing function to create a low-key springy effect. 

.scroll-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), 0px); /* serves the open and close actions */
  transition: row-gap .3s .1s cubic-bezier(.8, .5, .2, 1.4);
  &:has(.open :checked) {
    /* open action */
    animation: open .3s ease-in-out forwards;
    .open { display: none; }
    /* styling */
    row-gap: 10px;
    .items { color: rgb(113 124 158); transition: color .3s .1s;}
    .close { color: black }
  }
  &:has(.close :checked) {
    /* close action */
    --int: 0;
    animation: close .3s ease-in-out forwards;
    /* styling */
    row-gap: 0;
    .items { color: transparent; transition: color .2s;}
  }
}

When expanded, the row gaps go up to 10px and the text color comes in. When shrinking, the row gaps go down to 0 and the text color fades out to transparent. With that, the example is complete! Here’s the Pen once more:

Note: You can try this method with any grid compositions — rows, columns, or both.

Further Reading

]]>
https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/feed/ 3 4128
How to Make a CSS Timer https://frontendmasters.com/blog/how-to-make-a-css-timer/ https://frontendmasters.com/blog/how-to-make-a-css-timer/#comments Wed, 29 May 2024 16:40:25 +0000 https://frontendmasters.com/blog/?p=2424 There are times when we can use a timer on websites. 😉

Perhaps we want to time a quiz or put pressure on a survey. Maybe we are just trying to making a dramatic countdown after a user has done something great like successfully booking a concert ticket. We could be trying to build a micro time management tool (think Pomodoro). Or it could just be an alternative to a spinner UI.

When those situations come up, I have no qualms employing JavaScript which is probably a more powerful tool for this sort of thing generally. And yet! CSS substitutes are just as fun and efficient when the simplest option is the best one. Some email clients these days are highly CSS capable, but would never run JavaScript, so perhaps that situation could be an interesting progressive enhancement.

Let’s take a look at what it takes to cook up a CSS timer. We’ll use some modern CSS tech to do it. The ingredients?

  1. CSS Counters
  2. @property
  3. pseudo elements
  4. @keyframes
  5. A little Celtic sea salt to taste

To get started, fire up a (Code)Pen and keep it warm. Below is the demo we’ll be working towards (later, I’ll show some stylized examples):

There are three main requirements for our CSS Timer:

  1. A number that can decrement from 5 to 0
  2. A way to time five seconds, and decrement the number in each
  3. A way to display the decreasing number on page 

The Number

For our first requirement, the update-able number, we’ll use @property to create a custom property that will hold a value of type <integer>

Note: Integer numbers can be zero, or a positive or negative whole number. If you want numbers with decimal points, use <number>, which holds a real number. 

@property --n {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}

The Counting

For tracking seconds, while decreasing the number, we go to @keyframes animation.

@keyframes count {
  from { --n: 5; }
  to   { --n: 0; }
}

The animation function is put to action with the animation property.

.timer:hover::after {
  animation: 5s linear count;
}

Here’s what’s happening: 

When we register a custom property for a specific value type, <integer>, <percentage>, or <color>, for instances, the browser knows that the property is created to work with that specific type of value. 

With that knowledge the browser confidently updates the custom property’s value in the future, even throughout an animation. 

That’s why our property --n can go from 5 to 0 within an animation, and since the animation is set for five seconds, that’s essentially counting from five to zero over a period of five seconds. Hence, a timer is born. 

But there’s still the matter of printing out the counted numbers onto the page. If you hadn’t noticed earlier, I’d assigned the animation to a pseudo-element, and that should give you a clue for our next move — content

The Display

The property, content, can display contents we have not yet added to the HTML ourselves. We generally use this property for a variety of things, because this accepts a variety of values — images, strings, counters, quotation marks, even attribute values. It doesn’t, however, directly takes a number. So, we’ll feed it our number --n through counter

A counter can be set with either counter-reset or counter-increment. We’ll use counter-reset. This property’s value is a counter name and an integer. Since counter-reset doesn’t correctly process a CSS variable or custom property for an integer yet, but does accept calc(), the calc() function becomes our Trojan Horse, inside of which we’ll send in –n. 

.timer:hover::after {
  animation: 5s linear count;
  animation-fill-mode: forwards; 
  counter-reset: n calc(0 + var(--n));
  content: counter(n);
}

That is:

  1. Our animate-able number, --n, is first fed to calc() 
  2. calc() is then fed to counter()
  3. The counter() in turn is given to content, finally rendering --n on the page. 

The rest is taken care of by the browser. It knows --n is an integer. The browser keeps up with animation changing this integer from 5 to 0 in five seconds. Then, because the integer is used in a content value, the browser displays the integer on the page as it updates.

At the end of the animation, the animation-fill-mode: forwards; style rule prevents the timer from reverting back to the initial --n value, zero, right away. 

Once again, here’s the final demo:

For design variants you can count up or down, or play with its appearance, or you can combine this with other typical loader or progress designs, like a circular animation

At the time of writing, Firefox is the only missing browser support for @property, but they have announced an intent to ship, so shouldn’t be long now. For support reference, here’s the caniuse.com page for @property

CSS Custom Properties can also be set and updated in JavaScript. So, if at some point you would like to be able to update the property in JavaScript, just about with any other CSS property, you can do it using the setProperty() function. And if you wish to create a new custom property in JavaScript, that can be done with registerProperty(). The other direction, if you wanted to let JavaScript know a CSS animation has completed, you could listen for the animationend end event.

If you’re really into this sort of thing, also check out Yuan Chuan’s recent Time-based CSS Animations article.

]]>
https://frontendmasters.com/blog/how-to-make-a-css-timer/feed/ 1 2424