back to blog

css-only anchor() based radio selector animation

Your browser doesn't support the anchor positioning API! Demos on this page will not work properly.

Trying to use the new anchor positioning in CSS can be a little intimidating at first. Once you do grasp the basics of it however, it's a very versatile and powerful attribute.

It allows us to position an element relative to another element no matter where it is in the viewport, which is mostly meant for hovering elements like tooltips.

However, with a little bit of creativity, it can be used to create some really interesting effects which previously were not possible without Javascript.

One of the main things which makes it so useful, is that the anchor which an element is bound to can be transitioned between. This allows us to easily transition the position of one element to that of another.

One of the first things I wanted to try with this approach, was to create a radio selector with a moving indicator:

What's your favourite color?
Codepen link

The anchor positioning API has quite a lot of features, but the most important ones for this demo are the following:

  • position-anchor & anchor-name, which lets us bind one element to another (the anchor).

  • anchor(), which lets us access different properties of the anchor, and use them in attributes on the bound element.

First things first; let's set up our radio inputs by creating a label for each option, and putting the input elements inside of them. We want to hide the original circle indicator, since we'll be using our own custom indicator instead, which we'll do by simply applying display: none to the inputs.

We'll also need to make an element to be our indicator. For the sake of this demo, I've chosen to just make it an empty div.

With this in place, our HTML should look something like this:

<span>What's your favourite color?</span>
<div class="radio-container">
  <label for="green">
    <input type="radio" name="demo1" id="green" checked />
    Green
  </label>
  <label for="blue">
    <input type="radio" name="demo1" id="blue" />
    Blue
  </label>
  <label for="yellow">
    <input type="radio" name="demo1" id="yellow" />
    Yellow
  </label>
  <div class="highlight"></div>
</div>

While our CSS should look something like this:

span {
  margin-bottom: 1.2rem;
}

.radio-container {
  display: flex;
  gap: 1rem;
}

label, span {
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}

input {
  display: none;
}

This gives us some nice basic styling to build upon, but you'll notice that nothing happens yet when we click on the radio inputs:

What's your favourite color?

To make it interactive, we'll now be using the anchor positioning API to anchor the position of our indicator element to the position of whichever radio input is currently checked.

The first step is establishing a link between the indicator element and the checked radio input, which is actually very simple.

First, we add the position-anchor attribute to the indicator element. This tells our indicator to bind to whatever element we specify. Do note that this needs to be a <dashed-indent>, which you've probably seen before when using custom properties. Something like --my-anchor should work for now.

Next, we add the anchor-name attribute to whatever input is currently checked by utilising the :checked selector. We'll want to set this one to --my-anchor also.

Now that our elements are linked, we can use the anchor() function to access the checked radio input's position.

We'll simply have to add the position: absolute to our indicator element, and set the top and left properties to values we read from anchor() function.

The anchor() function actually lets us read several different properties, like top, left, right, bottom and center.

In our case, since we want the highlight element to be in the exact same position as the label, we'll simply want to add top: anchor(top); and left: anchor(left); to our indicator element.

We'll also want to add a width and height to our indicator element, so that it's visible. This we will also read from the anchor, using the anchor-size() function.

Simply adding width: anchor-size(width); and height: anchor-size(height); to our indicator element will give us a highlight element which is always the same size as the label.

Finally, we'll want to make sure that the highlight is actually behind the label, so we'll be adding z-index: 1; to our label element.

Let's add this to our CSS:

.highlight {
  position: absolute;
  left: anchor(left);
  top: anchor(top);
  width: anchor-size(width);
  height: anchor-size(height);
  position-anchor: --my-anchor;
  background-color: #00ff00;
}

label:has(input:checked) {
  anchor-name: --my-anchor;
}

label, span {
  z-index: 1;
}
What's your favourite color?

It works! Great!

Now let's make it a little more interesting by adding a transition to the highlight element, and also transitioning the colour based on the option selected.

To start with, let's add a transition to our highlight element, so that it animates smoothly when the radio input is checked. Something like transition: 0.3s ease-in-out; should do the trick.

Then, let's add a new custom property to our container element, which will be our current highlight colour. We'll call this --highlight-color, and set it to #00ff00initially.

Next, we'll want to add some selectors to our CSS which will change this custom property based on the option selected. This is slightly more complex than it seems, since we want to change the property on the container, and not on the labels.

For this, we'll be using the :has selector, to check whether container has a label in it which has a checked input as its first child. We'll want to have a different selector for each option, which should look something like .radio-container:has(label:nth-of-type(3) > input:checked), changing the --highlight-color property to to whatever colour is associated with this label.

And finally, we'll want to add a background-color to our highlight element to var(--highlight-color);

Adding this to our CSS should look something like this:

.radio-container {
  --highlight-color: transparent;
}

.highlight {
  background-color: var(--highlight-color);
  transition: 0.3s ease-in-out;
}

.radio-container:has(label:nth-of-type(1) > input:checked) {
  --highlight-color: #00ff00; /* Green */
}

.radio-container:has(label:nth-of-type(2) > input:checked) {
  --highlight-color: #0000ff; /* Blue */
}

.radio-container:has(label:nth-of-type(3) > input:checked) {
  --highlight-color: #ffff00; /* Yellow */
}

  
What's your favourite color?

Works great! But, we're forgetting something... what happens if the browser doesn't support the anchor positioning API?

Well, not much of anything. And that's a problem! There'll no indication for the user on what the currently active input is!

To fix this, we'll need to add a fallback for the anchor positioning API using the @supports rule.

Let's make it so that if the browser doesn't support the anchor positioning API, we'll just change the background colour of whatever the active input is and make the highlight element invisible.

We can do this by first adding some selectors to our CSS which will change the background colour of the active input and make the highlight element invisible, and then, after checking whether the browser supports the anchor positioning API, we'll add a @supports rule which will change the background colour back to the default, and make the highlight visible!

We'll want to replace our previous .radio-container:has(label:nth-of-type(n) > input:checked) selectors with the following:

label:nth-of-type(1):has(input:checked) {
  background-color: var(--green);
}
    
label:nth-of-type(2):has(input:checked) {
  background-color: var(--blue);
}
    
label:nth-of-type(3):has(input:checked) {
  background-color: var(--yellow);
}
    
@supports (position-anchor: --demo1-anchor) {
  .radio-container:has(label:nth-of-type(1) > input:checked) {
    --highlight-color: var(--green);
  }
    
  .radio-container:has(label:nth-of-type(2) > input:checked) {
    --highlight-color: var(--blue);
  }
    
  .radio-container:has(label:nth-of-type(3) > input:checked) {
    --highlight-color: var(--yellow);
  }
    
  label:nth-of-type(1):has(input:checked) {
    background-color: transparent;
  }
    
  label:nth-of-type(2):has(input:checked) {
    background-color: transparent;
  }
    
  label:nth-of-type(3):has(input:checked) {
    background-color: transparent;
  }
}
What's your favourite color?
Codepen link

And there it is! A smooth, elegant, colour changing radio selector with a moving highlight element.

This is a pretty simple example, but it shows the power of the anchor positioning API, and how it can be used to create some really interesting effects.

I've also made a codepen of this, so you can play around with it and see how it works.

Here's a couple more demos to get some more inspiration on what can be done with the anchor positioning API, feel free to look around the code in the codepens, and make sure to experiment with it yourself!

Nav menu - Codepen link
What's your favourite food?
Radio inputs - Codepen link
Hover over this demo to see the magic
CSS-Only mouse tracking experiment - Codepen link