Skip to content

Radio Traveling Dot

One shared dot glides to the checked radio with anchor(), with no script. Browsers without anchor positioning keep the radios' own ring.

Reduced Motion

Your system is set to reduce motion, so the animations shown on this page will not play for you.

Instantly Daily digest Weekly summary Never

Code

The following example assumes that you have imported the component and set up the theme.

Traveling Dot
<div class="traveling-dot" aria-label="Notifications" role="radiogroup">
7 collapsed lines
<sv-radio checked name="notifications" value="instant">Instantly</sv-radio>
<sv-radio name="notifications" value="daily">Daily digest</sv-radio>
<sv-radio name="notifications" value="weekly">Weekly summary</sv-radio>
<sv-radio name="notifications" value="never">Never</sv-radio>
<!-- The dot comes after the radios, because an anchor must be laid out first. -->
<span class="dot" aria-hidden="true"></span>
</div>
<style>
52 collapsed lines
.traveling-dot {
--ring-size: 1.3em;
--dot-size: 0.6em;
position: relative;
display: grid;
gap: 0.9em;
inline-size: fit-content;
padding-block: 1.5em;
margin-inline: auto;
sv-radio {
--sv-radio-sz: var(--ring-size);
}
.dot {
display: none;
}
@supports (anchor-name: --checked-radio) {
sv-radio {
--sv-radio-active-bdr-sz: 1.5px;
}
sv-radio:state(--checked) {
anchor-name: --checked-radio;
}
.dot {
position: absolute;
inset-block-start: calc(anchor(center) - var(--dot-size) / 2);
inset-inline-start: calc(anchor(start) + (var(--ring-size) - var(--dot-size)) / 2);
inline-size: var(--dot-size);
block-size: var(--dot-size);
position-anchor: --checked-radio;
pointer-events: none;
background-color: var(--sv-accent);
border-radius: 50%;
transition: inset 450ms cubic-bezier(0.3, 1.3, 0.5, 1);
}
&:has(sv-radio:state(--checked)) .dot {
display: block;
}
}
@media (prefers-reduced-motion: reduce) {
.dot {
transition-duration: 0s;
}
}
}
</style>