Tag: Animated

A CSS-only, animated, wrapping underline

Nicky Meuleman, inspired by Cassie Evans, details how they built the anchor link hover on their sites. When a link is hovered, another color underline kinda slides in with a gap between the two. Typical text-decoration doesn’t help here, so multiple backgrounds are used instead, and fortunately, it works with text that breaks across multiple lines as well.

Direct Link to ArticlePermalink

The post A CSS-only, animated, wrapping underline appeared first on CSS-Tricks.

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


, , ,

Animated Matryoshka Dolls in CSS

Here’s a fun one. How might we create a set of those cool Matryoshka dolls where they nest inside one another… but in CSS?

I toyed with this idea in my head for a little while. Then, I saw a tweet from CSS-Tricks and the article image had the dolls. I took that as a sign! It was time to put fingers to the keyboard.

Our goal here is to make these fun and interactive, where we can click on a doll to open it up and reveal another, smaller doll. Oh, and stick with just CSS for the functionality. And while we’re at it, let’s replace the dolls with our own character, say a CodePen bear. Something like this:

We won’t dwell on making things pretty to start. Let’s get some markup on the page and thrash out the mechanics first.

We can’t have an infinite amount of dolls. When we reach the innermost doll, it’d be nice to be able to reset the dolls without having to do a page refresh. A neat trick for this is to wrap our scene in an HTML form. That way we can use an input and set the type attribute to reset to avoid using any JavaScript.

<form>   <input type="reset" id="reset"/>   <label for="reset" title="Reset">Reset</label> </form>

Next, we need some dolls. Or bears. Or something to start with. The key here will be to use the classic checkbox hack and any associated form labels. As a note, I’m going to use Pug to handle the markup because it supports loops, making things a little easier. But, you can certainly write the HTML by hand. Here’s the start with form fields and labels that set up the checkbox hack.

Try clicking some of the inputs and hitting the Reset input. They all become unchecked. Nice, we’ll use that.

We have some interactivity but nothing is really happening yet. Here’s the plan:

  1. We’ll only show one checkbox at a time
  2. Checking a checkbox should reveal the label for the next checkbox.
  3. When we get to the last checkbox, there our only option should be to reset the form.

The trick will be to make use of the CSS adjacent sibling combinator (+).

input:checked + label + input + label {   display: block; }

When a checkbox is checked, we need to show the label for the next doll, which will be three siblings along in the DOM. How do we make the first label visible? Give it an explicit display: block via inline styles in our markup. Putting this together, we have something along these lines:

Clicking each label reveals the next. Hold on, the last label isn’t shown! That’s correct. And that’s because the last label doesn’t have a checkbox. We need to add a rule that caters to that last label.

input:checked + label + input + label, input:checked + label + label {   display: block; }

Cool. We’re getting somewhere. That’s the basic mechanics. Now things are going to get a little trickier. 

Basic styling

So, you might be thinking, “Why aren’t we hiding the checked label?” Good question! But, if we hide it straight away, we won’t have any transition between the current doll and the next. Before we start animating our dolls, let’s create basic boxes that will represent a doll. We can style them up so they mimic the doll outline without the detail.

.doll {   color: #fff;   cursor: pointer;   height: 200px;   font-size: 2rem;   left: 50%;   position: absolute;   text-align: center;   top: 50%;   transform: translate(-50%, -50%);   width: 100px; }  .doll:nth-of-type(even) {   background: #00f; }  .doll:nth-of-type(odd) {   background: #f00; }

Clicking one doll instantly reveals the next one and, when we’ve reached the last doll, we can reset the form to start again. That’s what we want here.

The mechanics

We are going to animate the dolls based on a center point. Our animation will consist of many steps:

  1. Slide the current doll out to the left.
  2. Open the doll to reveal the next one.
  3. Move the next doll where the current one started.
  4. Make the current doll fade out.
  5. Assign the next doll as the current doll.

Let’s start by sliding the current doll out to the left. We apply an animation when we click a label. Using the :checked pseudo-selector we can target the current doll. At this point, it’s worth noting that we are going to use CSS variables to control animation speed and behavior. This will make it easier to chain animations on the labels.

:root {   --speed: 0.25;   --base-slide: 100;   --slide-distance: 60; }  input:checked + label {   animation: slideLeft calc(var(--speed) * 1s) forwards; }  @keyframes slideLeft {   to {     transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -1%), 0);   } }

That looks great. But there’s an issue. As soon as we click a label, we could click it again and reset the animation. We don’t want that to happen.

How can we get around this? We can remove pointer events from a label once it’s been clicked.

input:checked + label {   animation: slideLeft calc(var(--speed) * 1s) forwards;   pointer-events: none; }

Great! Now once we have started, we can’t stop the animation chain from happening.

Next up, we need to crack open the doll to reveal the next one. This is where things get tricky because we are going to need some extra elements, not only to create the effect that the doll is opening up, but also to reveal the next doll inside of it. That’s right: we need to duplicate the inner doll. The trick here is to reveal a “fake” doll that we swap for the real one once we’ve animated it. This also means delaying the reveal of the next label.

Now our markup updates labels so that they contains span elements.

<label class="doll" for="doll--1">   <span class="doll doll--dummy"></span>   <span class="doll__half doll__half--top">Top</span>   <span class="doll__half doll__half--bottom">Bottom</span> </label>

These will act as the “dummy” doll as well as the lid and base for the current doll.

.doll {   color: #fff;   cursor: pointer;   height: 200px;   font-size: 2rem;   position: absolute;   text-align: center;   width: 100px; }  .doll:nth-of-type(even) {   --bg: #00f;   --dummy-bg: #f00; }  .doll:nth-of-type(odd) {   --bg: #f00;   --dummy-bg: #00f; }  .doll__half {   background: var(--bg);   position: absolute;   width: 100%;   height: 50%;   left: 0; }  .doll__half--top {   top: 0; }  .doll__half--bottom {   bottom: 0; }  .doll__dummy {   background: var(--dummy-bg);   height: 100%;   width: 100%;   position: absolute;   top: 0;   left: 0; }

The lid requires three translations to create the opening effect: one to pop it up, one to move it left and then one to pop it down.

@keyframes open {   0% {     transform: translate(0, 0);   }   33.333333333333336% {     transform: translate(0, -100%);   }   66.66666666666667% {     transform: translate(-100%, -100%);   }   100% {     transform: translate(-100%, 100%);   } }

Next is where we can use CSS custom properties to handle changing values. Once the doll has slid over to the left, we can open it. But how do we know how long to delay it from opening until that happens? We can use the --speed custom property we defined earlier to calculate the correct delay.

It looks a little quick if we use the --speed value as it is, so let’s multiply it by two seconds:

input:checked + .doll {   animation: slideLeft calc(var(--speed) * 1s) forwards;   pointer-events: none; }  input:checked + .doll .doll__half--top {   animation: open calc(var(--speed) * 2s) calc(var(--speed) * 1s) forwards; // highlight }

Much better:

Now we need to move the inner “dummy” doll to the new position. This animation is like the open animation in that it consists of three stages. Again, that’s one to move up, one to move right, and one to set down. It’s like the slide animation, too. We are going to use CSS custom properties to determine the distance that the doll moves.

:root {   // Introduce a new variable that defines how high the dummy doll should pop out.   --pop-height: 60; }  @keyframes move {   0% {     transform: translate(0, 0) translate(0, 0);   }   33.333333333333336% {     transform: translate(0, calc(var(--pop-height) * -1%)) translate(0, 0);   }   66.66666666666667% {     transform: translate(0, calc(var(--pop-height) * -1%)) translate(calc((var(--base-slide) * 1px) + var(--slide-distance) * 1%), 0);   }   100% {     transform: translate(0, calc(var(--pop-height) * -1%)) translate(calc((var(--base-slide) * 1px) + var(--slide-distance) * 1%), calc(var(--pop-height) * 1%));   } }

Almost there! 

The only thing is that the next doll is available as soon as we click a doll. that means we can spam click our way through the set.

Technically, the next doll shouldn’t show until the “fake” one has moved into place. It’s only once the “fake” doll is in place that we can hide it and reveal the real one. That means we going to use zero-second scale animations! That’s right. We can play pretend by delaying two zero-second animations and using animation-fill-mode.

@keyframes appear {   from {     transform: scale(0);   } }

We actually only need one set of @keyframes. because we can re-use what we have to create the opposite movement with animation-direction: reverse. With that in mind, all our animations get applied something like this:

// The next doll input:checked + .doll + input + .doll, // The last doll (doesn't have an input) input:checked + .doll + .doll {   animation: appear 0s calc(var(--speed) * 5s) both;   display: block; }  // The current doll input:checked + .doll, // The current doll that isn't the first. Specificity prevails input:checked + .doll + input:checked + .doll {   animation: slideLeft calc(var(--speed) * 1s) forwards;   pointer-events: none; }  input:checked + .doll .doll__half--top, input:checked + .doll + input:checked + .doll .doll__half--top {   animation: open calc(var(--speed) * 2s) calc(var(--speed) * 1s) forwards; }  input:checked + .doll .doll__dummy, input:checked + .doll + input:checked + .doll .doll__dummy {   animation: move calc(var(--speed) * 2s) calc(var(--speed) * 3s) forwards, appear 0s calc(var(--speed) * 5s) reverse forwards; }

Note how important the variables are, especially where we are chaining animations. That gets us almost where we need to be.

I can hear it now: “They’re all the same size!” Yep. That’s the missing piece. They need to scale down. The trick here is to adjust the markup again and make use of CSS custom properties yet again.

<input id="doll--0" type="checkbox"/> <label class="doll" for="doll--0" style="display: block; --doll-index: 0;">   <span class="doll__dummy-container">     <span class="doll__dummy"></span>   </span> //highlight   <span class="doll__container">     <span class="doll__half doll__half--top"></span>     <span class="doll__half doll__half--bottom"></span>   </span> </label>

We just introduced a CSS custom property inline that tells us the index of the doll. We can use this to generate a scale of each half as well as the fake inner doll. The halves will have to scale to match the actual doll size, but the fake inner doll scale will need to match that of the next doll. Tricky!

We can apply these scales inside the containers so that our animations are not affected.

:root {   --scale-step: 0.05; }  .doll__container, .doll__dummy-container {   height: 100%;   left: 0;   position: absolute;   top: 0;   width: 100%; }  .doll__container {   transform: scale(calc(1 - ((var(--doll-index)) * var(--scale-step))));   transform-origin: bottom; }  .doll__dummy {   height: 100%;   left: 0;   position: absolute;   top: 0;   transform: scale(calc(1 - ((var(--doll-index) + 1) * var(--scale-step))));   transform-origin: bottom center;   width: 100%; }

Note how the .doll__dummy class uses var(--doll-index) + 1) to calculate the scale so that it matches the next doll.  👍

Lastly, we re-assign the animation to the .doll__dummy-container class instead of the .doll__dummy class.

input:checked + .doll .doll__dummy-container, input:checked + .doll + input:checked + .doll .doll__dummy-container {   animation: move calc(var(--speed) * 2s) calc(var(--speed) * 3s) forwards, appear 0s calc(var(--speed) * 5s) reverse forwards; }

Here’s a demo where the containers have been given a background color to see what’s happening.

We can see that, although the content size changes, they remain the same size. This makes for consistent animation behavior and makes the code easier to maintain.

Finishing touches

Wow, things are looking pretty slick! All we need are some finishing touches and we are done!

The scene starts to look cluttered because we’re stacking the “old” dolls off to the side when a new one is introduced. So let’s slide a doll out of view when the next one is revealed to clean that mess up.

@keyframes slideOut {   from {     transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -1%), 0);   }   to {     opacity: 0;     transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -2%), 0);   } }  input:checked + .doll, input:checked + .doll + input:checked + .doll {   animation: slideLeft calc(var(--speed) * 1s) forwards,     slideOut calc(var(--speed) * 1s) calc(var(--speed) * 6s) forwards;   pointer-events: none; }

The new slideOut animation fades the doll out while it translates to the left. Perfect.  👍

That’s it for the CSS trickery we need to make this effect work. All that’s left style the dolls and the scene.

We have many options to style the dolls. We could use a background image, CSS illustration, SVG, or what have you. We could even throw together some emoji dolls that use random inline hues!

Let’s go with inline SVG.

I’m basically using the same underlying mechanics we’ve already covered. The difference is that I’m also generating inline variables for hue and lightness so the bears sport different shirt colors.

There we have it! Stacking dolls — err, bears — with nothing but HTML and CSS! All the code for all the steps is available in this CodePen collection. Questions or suggestions? Feel free to reach out to me here in the comments.

The post Animated Matryoshka Dolls in CSS appeared first on CSS-Tricks.


, ,

How to Create an Animated Countdown Timer With HTML, CSS and JavaScript

Have you ever needed a countdown timer on a project? For something like that, it might be natural to reach for a plugin, but it’s actually a lot more straightforward to make one than you might think and only requires the trifecta of HTML, CSS and JavaScript. Let’s make one together!

This is what we’re aiming for:

Here are a few things the timer does that we’ll be covering in this post:

  • Displays the initial time remaining
  • Converts the time value to a MM:SS format
  • Calculates the difference between the initial time remaining and how much time has passed
  • Changes color as the time remaining nears zero
  • Displays the progress of time remaining as an animated ring

OK, that’s what we want, so let’s make it happen!

Step 1: Start with the basic markup and styles

Let’s start with creating a basic template for our timer. We will add an svg with a circle element inside to draw a timer ring that will indicate the passing time and add a span to show the remaining time value. Note that we’re writing the HTML in JavaScript and injecting into the DOM by targeting the #app element. Sure, we could move a lot of it into an HTML file, if that’s more your thing.

document.getElementById("app").innerHTML = ` <div class="base-timer">   <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">     <g class="base-timer__circle">       <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45" />     </g>   </svg>   <span>     <!-- Remaining time label -->   </span> </div> `;

Now that we have some markup to work with, let’s style it up a bit so we have a good visual to start with. Specifically, we’re going to:

  • Set the timer’s size
  • Remove the fill and stroke from the circle wrapper element so we get the shape but let the elapsed time show through
  • Set the ring’s width and color
/* Sets the containers height and width */ .base-timer {   position: relative;   height: 300px;   width: 300px; }  /* Removes SVG styling that would hide the time label */ .base-timer__circle {   fill: none;   stroke: none; }  /* The SVG path that displays the timer's progress */ .base-timer__path-elapsed {   stroke-width: 7px;   stroke: grey; }

Having that done we end up with a basic template that looks like this.

Step 2: Setting up the time label

As you probably noticed, the template includes an empty <span> that’s going to hold the time remaining. We will fill that place with a proper value. We said earlier that the time will be in MM:SS format. To do that we will create a method called formatTimeLeft:

function formatTimeLeft(time) {   // The largest round integer less than or equal to the result of time divided being by 60.   const minutes = Math.floor(time / 60);      // Seconds are the remainder of the time divided by 60 (modulus operator)   let seconds = time % 60;      // If the value of seconds is less than 10, then display seconds with a leading zero   if (seconds < 10) {     seconds = `0$ {seconds}`;   }    // The output in MM:SS format   return `$ {minutes}:$ {seconds}`; }

Then we will use our method in the template:

document.getElementById("app").innerHTML = ` <div class="base-timer">   <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">     <g class="base-timer__circle">       <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>     </g>   </svg>   <span id="base-timer-label" class="base-timer__label">     $ {formatTime(timeLeft)}   </span> </div> `

To show the value inside the ring we need to update our styles a bit.

.base-timer__label {   position: absolute;      /* Size should match the parent container */   width: 300px;   height: 300px;      /* Keep the label aligned to the top */   top: 0;      /* Create a flexible box that centers content vertically and horizontally */   display: flex;   align-items: center;   justify-content: center;    /* Sort of an arbitrary number; adjust to your liking */   font-size: 48px; }

OK, we are ready to play with the timeLeft  value, but the value doesn’t exist yet. Let’s create it and set the initial value to our time limit.

// Start with an initial value of 20 seconds const TIME_LIMIT = 20;  // Initially, no time has passed, but this will count up // and subtract from the TIME_LIMIT let timePassed = 0; let timeLeft = TIME_LIMIT;

And we are one step closer.

Right on! Now we have a timer that starts at 20 seconds… but it doesn’t do any counting just yet. Let’s bring it to life so it counts down to zero seconds.

Step 3: Counting down

Let’s think about what we need to count down the time. Right now, we have a timeLimit value that represents our initial time, and a timePassed value that indicates how much time has passed once the countdown starts.

What we need to do is increase the value of timePassed by one unit per second and recompute the timeLeft value based on the new timePassed value. We can achieve that using the setInterval function.

Let’s implement a method called startTimer that will:

  • Set counter interval
  • Increment the timePassed value each second
  • Recompute the new value of timeLeft
  • Update the label value in the template

We also need to keep the reference to that interval object to clear it when needed — that’s why we will create a timerInterval variable.

let timerInterval = null;  document.getElementById("app").innerHTML = `...`  function startTimer() {   timerInterval = setInterval(() => {          // The amount of time passed increments by one     timePassed = timePassed += 1;     timeLeft = TIME_LIMIT - timePassed;          // The time left label is updated     document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);   }, 1000); }

We have a method that starts the timer but we do not call it anywhere. Let’s start our timer immediately on load.

document.getElementById("app").innerHTML = `...` startTimer();

That’s it! Our timer will now count down the time. While that’s great and all, it would be nicer if we could add some color to the ring around the time label and change the color at different time values.

Step 4: Cover the timer ring with another ring

To visualize time passing, we need to add a second layer to our ring that handles the animation. What we’re doing is essentially stacking a new green ring on top of the original gray ring so that the green ring animates to reveal the gray ring as time passes, like a progress bar.

Let’s first add a path element in our SVG element.

document.getElementById("app").innerHTML = ` <div class="base-timer">   <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">     <g class="base-timer__circle">       <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>       <path         id="base-timer-path-remaining"         stroke-dasharray="283"         class="base-timer__path-remaining $ {remainingPathColor}"         d="           M 50, 50           m -45, 0           a 45,45 0 1,0 90,0           a 45,45 0 1,0 -90,0         "       ></path>     </g>   </svg>   <span id="base-timer-label" class="base-timer__label">     $ {formatTime(timeLeft)}   </span> </div> `;

Next, let’s create an initial color for the remaining time path.

const COLOR_CODES = {   info: {     color: "green"   } };  let remainingPathColor = COLOR_CODES.info.color;

Finally, let’s add few styles to make the circular path look like our original gray ring. The important thing here is to make sure the stroke-width is the same size as the original ring and that the duration of the transition is set to one second so that it animates smoothly and corresponds with the time remaining in the time label.

.base-timer__path-remaining {   /* Just as thick as the original ring */   stroke-width: 7px;    /* Rounds the line endings to create a seamless circle */   stroke-linecap: round;    /* Makes sure the animation starts at the top of the circle */   transform: rotate(90deg);   transform-origin: center;    /* One second aligns with the speed of the countdown timer */   transition: 1s linear all;    /* Allows the ring to change color when the color value updates */   stroke: currentColor; }  .base-timer__svg {   /* Flips the svg and makes the animation to move left-to-right */   transform: scaleX(-1); }

This will output a stroke that covers the timer ring like it should, but it doesn’t animate just yet to reveal the timer ring as time passes.

To animate the length of the remaining time line we are going to use the stroke-dasharray property. Chris explains how it’s used to create the illusion of an element “drawing” itself. And there’s more detail about the property and examples of it in the CSS-Tricks almanac.

Step 5: Animate the progress ring

Let’s see how our ring will look like with different stroke-dasharray values:

What we can see is that the value of stroke-dasharray is actually cutting our remaining time ring into equal-length sections, where the length is the time remaining value. That is happening when we set the value of stroke-dasharray to a single-digit number (i.e. 1-9).

The name dasharray suggests that we can set multiple values as an array. Let’s see how it will behave if we set two numbers instead of one; in this case, those values are 10 and 30.

stroke-dasharray: 10 30

That sets the first section (remaining time) length to 10 and the second section (passed time) to 30. We can use that in our timer with a little trick. What we need initially is for the ring to cover the full length of the circle, meaning the remaining time equals the length of our ring.

What’s that length? Get out your old geometry textbook, because we can calculate the length an arc with some math:

Length = 2πr = 2 * π * 45 = 282,6

That’s the value we want to use when the ring initially mounted. Let’s see how it looks.

stroke-dasharray: 283 283

That works!

OK, the first value in the array is our remaining time, and the second marks how much time has passed. What we need to do now is to manipulate the first value. Let’s see below what we can expect when we change the first value.

We will create two methods, one responsible for calculating what fraction of the initial time is left, and one responsible for calculating the stroke-dasharray value and updating the <path> element that represents our remaining time.

// Divides time left by the defined time limit. function calculateTimeFraction() {   return timeLeft / TIME_LIMIT; }      // Update the dasharray value as time passes, starting with 283 function setCircleDasharray() {   const circleDasharray = `$ {(     calculateTimeFraction() * FULL_DASH_ARRAY   ).toFixed(0)} 283`;   document     .getElementById("base-timer-path-remaining")     .setAttribute("stroke-dasharray", circleDasharray); }

We also need to update our path each second that passes. That means we need to call the newly created setCircleDasharray method inside our timerInterval.

function startTimer() {   timerInterval = setInterval(() => {     timePassed = timePassed += 1;     timeLeft = TIME_LIMIT - timePassed;     document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);          setCircleDasharray();   }, 1000); }

Now we can see things moving!

Woohoo, it works… but… look closely, especially at the end. It looks like our animation is lagging by one second. When we reach 0 a small piece of the ring is still visible.

This is due to the animation’s duration being set to one second. When the value of remaining time is set to zero, it still takes one second to actually animate the ring to zero. We can get rid of that by reducing the length of the ring gradually during the countdown. We do that in our calculateTimeFraction method.

function calculateTimeFraction() {   const rawTimeFraction = timeLeft / TIME_LIMIT;   return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction); }

There we go!

Oops… there is one more thing. We said we wanted to change the color of the progress indicator when when the time remaining reaches certain points — sort of like letting the user know that time is almost up.

Step 6: Change the progress color at certain points of time

First, we need to add two thresholds that will indicate when we should change to the warning and alert states and add colors for each of that states. We’re starting with green, then go to orange as a warning, followed by red when time is nearly up.

// Warning occurs at 10s const WARNING_THRESHOLD = 10; // Alert occurs at 5s const ALERT_THRESHOLD = 5;  const COLOR_CODES = {   info: {     color: "green"   },   warning: {     color: "orange",     threshold: WARNING_THRESHOLD   },   alert: {     color: "red",     threshold: ALERT_THRESHOLD   } };

Now, let’s create a method that’s responsible for checking if the threshold exceeded and changing the progress color when that happens.

function setRemainingPathColor(timeLeft) {   const { alert, warning, info } = COLOR_CODES;    // If the remaining time is less than or equal to 5, remove the "warning" class and apply the "alert" class.   if (timeLeft <= alert.threshold) {     document       .getElementById("base-timer-path-remaining")       .classList.remove(warning.color);     document       .getElementById("base-timer-path-remaining")       .classList.add(alert.color);    // If the remaining time is less than or equal to 10, remove the base color and apply the "warning" class.   } else if (timeLeft <= warning.threshold) {     document       .getElementById("base-timer-path-remaining")       .classList.remove(info.color);     document       .getElementById("base-timer-path-remaining")       .classList.add(warning.color);   } }

So, we’re basically removing one CSS class when the timer reaches a point and adding another one in its place. We’re going to need to define those classes.

.base-timer__path-remaining.green {   color: rgb(65, 184, 131); }  .base-timer__path-remaining.orange {   color: orange; }  .base-timer__path-remaining.red {   color: red; }

Voilà, there we have it. Here’s the demo again with everything put together.

The post How to Create an Animated Countdown Timer With HTML, CSS and JavaScript appeared first on CSS-Tricks.


, , , , ,

Animated Position of Focus Ring

Maurice Mahan created FocusOverlay, a “library for creating overlays on focused elements.” That description is a little confusing at you don’t need a library to create focus styles. What the library actually does is animate the focus rings as focus moves from one element to another. It’s based on the same idea as Flying Focus.

I’m not strong enough in my accessibility knowledge to give a definitive answer if this is a great idea or not, but my mind goes like this:

  • It’s a neat effect.
  • I can imagine it being an accessibility win since, while the page will scroll to make sure the next focused element is visible, it doesn’t otherwise help you see where that focus has gone. Movement that directs attention toward the next focused element may help make it more clear.
  • I can imagine it being harmful to accessibility in that it is motion that isn’t usually there and could be surprising.

On that last point, you could conditionally load it depending on a user’s motion preference.

The library is on npm, but is also available as direct linkage thanks to UNPKG. Let’s look at using the URLs to the resources directly to illustrate the concept of conditional loading:

<link    rel="stylesheet"    href="//unpkg.com/focus-overlay@latest/dist/focusoverlay.css"    media="prefers-reduced-motion: no-preference" />  <script> const mq = window.matchMedia("(prefers-reduced-motion: no-preference)");  if (mq.matches) {   let script = document.createElement("script");   script.src = "//unpkg.com/focus-overlay@latest/dist/focusoverlay.js";   document.head.appendChild(script); } </script>

The JavaScript is also 11.5 KB / 4.2 KB compressed and the CSS is 453 B / 290 B compressed, so you’ve always got to factor that into as performance and accessibility are related concepts.

Performance isn’t just script size either. Looking through the code, it looks like the focus ring is created by appending a <div> to the <body> that has a super high z-index value in which to be seen and pointer-events: none as to not interfere. Then it is absolutely positioned with top and left values and sized with width and height. It looks like new positional information is calculated and then applied to this div, and CSS handles the movement. Last I understood, those aren’t particularly performant CSS properties to animate, so I would think a future feature here would be to use animation FLIP to take advantage of only animating transforms.

The post Animated Position of Focus Ring appeared first on CSS-Tricks.


, , ,