Eye Chart
import { useId } from 'react';
const ROWS = [
{ value: 'huge', label: 'Huge', letters: 'E', size: 40, h: 42, tip: 96 },
{ value: 'large', label: 'Large', letters: 'FP', size: 27, h: 28, tip: 103 },
{ value: 'default', label: 'Default', letters: 'TOZ', size: 19, h: 20, tip: 105 },
{ value: 'small', label: 'Small', letters: 'LPED', size: 14, h: 15, tip: 105 },
{ value: 'tiny', label: 'Tiny', letters: 'PECFD', size: 10, h: 11, tip: 102 },
];
const PAD = 12;
const GAP = 6;
const TOPS = ROWS.reduce<number[]>((t, r, i) => [...t, i ? t[i - 1] + ROWS[i - 1].h + GAP : PAD], []);
const at = (n: number) => `.radio-eye-chart:has(.radio-eye-chart__row:nth-child(${n + 1}) input:checked)`;
const SPRING = 'linear(0, 0.111, 0.359, 0.638, 0.879, 1.047, 1.137, 1.163, 1.145, 1.105, 1.059, 1.02, 0.992, 0.978, 0.973, 0.976, 0.983, 0.99, 0.997, 1.001, 1.004, 1.004, 1.004, 1.003, 1)';
// Every row blurs by how far it is from the one in focus, like a lens racking.
const focus = ROWS.map((_, c) =>
ROWS.map((_, k) => {
const d = Math.abs(k - c);
return `${at(c)} .radio-eye-chart__row:nth-child(${k + 1}) .radio-eye-chart__letters { filter: blur(${(d * 1.15).toFixed(2)}px); opacity: ${(1 - d * 0.1).toFixed(2)}; }`;
}).join('\n '),
).join('\n ');
const css = `
.radio-eye-chart { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); display: grid; justify-items: center; gap: 10px; font-family: Georgia, 'Times New Roman', serif; }
.radio-eye-chart__stage { position: relative; width: 230px; padding-left: 20px; }
.radio-eye-chart__chart { position: relative; display: block; width: 150px; padding: ${PAD}px 0; border-radius: 2px; background: var(--paper); color: var(--ink); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)), 0 10px 20px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.radio-eye-chart__row { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: ${GAP}px; cursor: pointer; }
.radio-eye-chart__row:last-of-type { margin-bottom: 0; }
.radio-eye-chart__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-eye-chart__letters { font-weight: 700; letter-spacing: 0.18em; margin-right: -0.18em; line-height: 1; transition: filter 0.7s cubic-bezier(0.3, 0, 0.2, 1), opacity 0.7s; }
.radio-eye-chart__row:hover .radio-eye-chart__letters { filter: blur(0) !important; opacity: 1 !important; transition-duration: 0.25s; }
.radio-eye-chart__row:has(input:focus-visible) .radio-eye-chart__letters { outline: 1.5px solid var(--ink); outline-offset: 3px; }
.radio-eye-chart__num { position: absolute; left: 8px; color: color-mix(in oklab, var(--ink) 55%, var(--paper)); font: 600 6.5px system-ui, sans-serif; }
${focus}
/* The line under the row in focus, and the pointer's tip, ride one spring. */
.radio-eye-chart__line { position: absolute; left: 14px; right: 14px; top: 0; height: 1.5px; background: var(--ink); transform: translateY(var(--ly)); transition: transform 0.6s ${SPRING}; pointer-events: none; }
.radio-eye-chart__pointer { position: absolute; left: 20px; top: 0; width: 104px; height: 4px; transform: translate(var(--px), var(--py)) rotate(-14deg); transform-origin: 0 50%; transition: transform 0.65s ${SPRING}; pointer-events: none; }
.radio-eye-chart__stick { display: block; width: 100%; height: 100%; border-radius: 2px 3px 3px 2px; background: linear-gradient(90deg, light-dark(#373a40, #f1f2f3) 0 9px, light-dark(#5f626a, #a7abb2) 9px); box-shadow: 0 3px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
${ROWS.map((r, i) => `${at(i)} { --ly: ${TOPS[i] + r.h + 2.5}px; --px: ${r.tip}px; --py: ${TOPS[i] + r.h / 2 - 2}px; }\n ${at(i)} .radio-eye-chart__stick { animation: radio-eye-chart-tap${i} 0.5s ease-out 0.5s; }\n @keyframes radio-eye-chart-tap${i} { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }`).join('\n ')}
.radio-eye-chart__read { display: grid; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; font-style: italic; }
.radio-eye-chart__read > span { grid-area: 1 / 1; text-align: center; opacity: 0; transform: translateY(4px); transition: opacity 0.2s, transform 0.3s; }
.radio-eye-chart__read b { color: light-dark(#373a40, #f1f2f3); font-style: normal; }
${ROWS.map((_, i) => `${at(i)} .radio-eye-chart__read > :nth-child(${i + 1})`).join(', ')} { opacity: 1; transform: none; transition-delay: 0.2s; }
@media (prefers-reduced-motion: reduce) {
.radio-eye-chart * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const name = useId();
return (
<>
<style href="kit-radios-eye-chart" precedence="kit">
{css}
</style>
<div className="radio-eye-chart">
<div className="radio-eye-chart__stage">
<div className="radio-eye-chart__chart" role="radiogroup" aria-label="Reading size">
{ROWS.map((r, i) => (
<label key={r.value} className="radio-eye-chart__row" style={{ height: r.h }}>
<input className="radio-eye-chart__input" type="radio" name={name} value={r.value} aria-label={r.label} defaultChecked={i === 2} />
<span className="radio-eye-chart__num" aria-hidden="true">
{i + 1}
</span>
<span className="radio-eye-chart__letters" style={{ fontSize: r.size }} aria-hidden="true">
{r.letters}
</span>
</label>
))}
<span className="radio-eye-chart__line" aria-hidden="true" />
</div>
<span className="radio-eye-chart__pointer" aria-hidden="true">
<span className="radio-eye-chart__stick" />
</span>
</div>
<p className="radio-eye-chart__read" aria-hidden="true">
{ROWS.map((r) => (
<span key={r.value}>
Reading size · <b>{r.label}</b>
</span>
))}
</p>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<div class="radio-eye-chart">
<div class="radio-eye-chart__stage">
<div class="radio-eye-chart__chart" role="radiogroup" aria-label="Reading size">
<label class="radio-eye-chart__row" style="height:42px">
<input class="radio-eye-chart__input" type="radio" aria-label="Huge" name="radio-eye-chart" value="huge"/>
<span class="radio-eye-chart__num" aria-hidden="true">1</span>
<span class="radio-eye-chart__letters" style="font-size:40px" aria-hidden="true">E</span>
</label>
<label class="radio-eye-chart__row" style="height:28px">
<input class="radio-eye-chart__input" type="radio" aria-label="Large" name="radio-eye-chart" value="large"/>
<span class="radio-eye-chart__num" aria-hidden="true">2</span>
<span class="radio-eye-chart__letters" style="font-size:27px" aria-hidden="true">FP</span>
</label>
<label class="radio-eye-chart__row" style="height:20px">
<input class="radio-eye-chart__input" type="radio" aria-label="Default" name="radio-eye-chart" checked="" value="default"/>
<span class="radio-eye-chart__num" aria-hidden="true">3</span>
<span class="radio-eye-chart__letters" style="font-size:19px" aria-hidden="true">TOZ</span>
</label>
<label class="radio-eye-chart__row" style="height:15px">
<input class="radio-eye-chart__input" type="radio" aria-label="Small" name="radio-eye-chart" value="small"/>
<span class="radio-eye-chart__num" aria-hidden="true">4</span>
<span class="radio-eye-chart__letters" style="font-size:14px" aria-hidden="true">LPED</span>
</label>
<label class="radio-eye-chart__row" style="height:11px">
<input class="radio-eye-chart__input" type="radio" aria-label="Tiny" name="radio-eye-chart" value="tiny"/>
<span class="radio-eye-chart__num" aria-hidden="true">5</span>
<span class="radio-eye-chart__letters" style="font-size:10px" aria-hidden="true">PECFD</span>
</label>
<span class="radio-eye-chart__line" aria-hidden="true"></span>
</div>
<span class="radio-eye-chart__pointer" aria-hidden="true">
<span class="radio-eye-chart__stick"></span>
</span>
</div>
<p class="radio-eye-chart__read" aria-hidden="true">
<span>
Reading size ·
<b>Huge</b>
</span>
<span>
Reading size ·
<b>Large</b>
</span>
<span>
Reading size ·
<b>Default</b>
</span>
<span>
Reading size ·
<b>Small</b>
</span>
<span>
Reading size ·
<b>Tiny</b>
</span>
</p>
</div>.radio-eye-chart { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); display: grid; justify-items: center; gap: 10px; font-family: Georgia, 'Times New Roman', serif; }
.radio-eye-chart__stage { position: relative; width: 230px; padding-left: 20px; }
.radio-eye-chart__chart { position: relative; display: block; width: 150px; padding: 12px 0; border-radius: 2px; background: var(--paper); color: var(--ink); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)), 0 10px 20px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.radio-eye-chart__row { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; cursor: pointer; }
.radio-eye-chart__row:last-of-type { margin-bottom: 0; }
.radio-eye-chart__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-eye-chart__letters { font-weight: 700; letter-spacing: 0.18em; margin-right: -0.18em; line-height: 1; transition: filter 0.7s cubic-bezier(0.3, 0, 0.2, 1), opacity 0.7s; }
.radio-eye-chart__row:hover .radio-eye-chart__letters { filter: blur(0) !important; opacity: 1 !important; transition-duration: 0.25s; }
.radio-eye-chart__row:has(input:focus-visible) .radio-eye-chart__letters { outline: 1.5px solid var(--ink); outline-offset: 3px; }
.radio-eye-chart__num { position: absolute; left: 8px; color: color-mix(in oklab, var(--ink) 55%, var(--paper)); font: 600 6.5px system-ui, sans-serif; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__row:nth-child(1) .radio-eye-chart__letters { filter: blur(0.00px); opacity: 1.00; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__row:nth-child(2) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__row:nth-child(3) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__row:nth-child(4) .radio-eye-chart__letters { filter: blur(3.45px); opacity: 0.70; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__row:nth-child(5) .radio-eye-chart__letters { filter: blur(4.60px); opacity: 0.60; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__row:nth-child(1) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__row:nth-child(2) .radio-eye-chart__letters { filter: blur(0.00px); opacity: 1.00; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__row:nth-child(3) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__row:nth-child(4) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__row:nth-child(5) .radio-eye-chart__letters { filter: blur(3.45px); opacity: 0.70; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__row:nth-child(1) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__row:nth-child(2) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__row:nth-child(3) .radio-eye-chart__letters { filter: blur(0.00px); opacity: 1.00; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__row:nth-child(4) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__row:nth-child(5) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__row:nth-child(1) .radio-eye-chart__letters { filter: blur(3.45px); opacity: 0.70; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__row:nth-child(2) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__row:nth-child(3) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__row:nth-child(4) .radio-eye-chart__letters { filter: blur(0.00px); opacity: 1.00; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__row:nth-child(5) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__row:nth-child(1) .radio-eye-chart__letters { filter: blur(4.60px); opacity: 0.60; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__row:nth-child(2) .radio-eye-chart__letters { filter: blur(3.45px); opacity: 0.70; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__row:nth-child(3) .radio-eye-chart__letters { filter: blur(2.30px); opacity: 0.80; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__row:nth-child(4) .radio-eye-chart__letters { filter: blur(1.15px); opacity: 0.90; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__row:nth-child(5) .radio-eye-chart__letters { filter: blur(0.00px); opacity: 1.00; }
/* The line under the row in focus, and the pointer's tip, ride one spring. */
.radio-eye-chart__line { position: absolute; left: 14px; right: 14px; top: 0; height: 1.5px; background: var(--ink); transform: translateY(var(--ly)); transition: transform 0.6s linear(0, 0.111, 0.359, 0.638, 0.879, 1.047, 1.137, 1.163, 1.145, 1.105, 1.059, 1.02, 0.992, 0.978, 0.973, 0.976, 0.983, 0.99, 0.997, 1.001, 1.004, 1.004, 1.004, 1.003, 1); pointer-events: none; }
.radio-eye-chart__pointer { position: absolute; left: 20px; top: 0; width: 104px; height: 4px; transform: translate(var(--px), var(--py)) rotate(-14deg); transform-origin: 0 50%; transition: transform 0.65s linear(0, 0.111, 0.359, 0.638, 0.879, 1.047, 1.137, 1.163, 1.145, 1.105, 1.059, 1.02, 0.992, 0.978, 0.973, 0.976, 0.983, 0.99, 0.997, 1.001, 1.004, 1.004, 1.004, 1.003, 1); pointer-events: none; }
.radio-eye-chart__stick { display: block; width: 100%; height: 100%; border-radius: 2px 3px 3px 2px; background: linear-gradient(90deg, light-dark(#373a40, #f1f2f3) 0 9px, light-dark(#5f626a, #a7abb2) 9px); box-shadow: 0 3px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) { --ly: 56.5px; --px: 96px; --py: 31px; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__stick { animation: radio-eye-chart-tap0 0.5s ease-out 0.5s; }
@keyframes radio-eye-chart-tap0 { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) { --ly: 90.5px; --px: 103px; --py: 72px; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__stick { animation: radio-eye-chart-tap1 0.5s ease-out 0.5s; }
@keyframes radio-eye-chart-tap1 { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) { --ly: 116.5px; --px: 105px; --py: 102px; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__stick { animation: radio-eye-chart-tap2 0.5s ease-out 0.5s; }
@keyframes radio-eye-chart-tap2 { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) { --ly: 137.5px; --px: 105px; --py: 125.5px; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__stick { animation: radio-eye-chart-tap3 0.5s ease-out 0.5s; }
@keyframes radio-eye-chart-tap3 { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) { --ly: 154.5px; --px: 102px; --py: 144.5px; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__stick { animation: radio-eye-chart-tap4 0.5s ease-out 0.5s; }
@keyframes radio-eye-chart-tap4 { 35% { translate: -5px 0; } 70% { translate: 1px 0; } }
.radio-eye-chart__read { display: grid; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; font-style: italic; }
.radio-eye-chart__read > span { grid-area: 1 / 1; text-align: center; opacity: 0; transform: translateY(4px); transition: opacity 0.2s, transform 0.3s; }
.radio-eye-chart__read b { color: light-dark(#373a40, #f1f2f3); font-style: normal; }
.radio-eye-chart:has(.radio-eye-chart__row:nth-child(1) input:checked) .radio-eye-chart__read > :nth-child(1), .radio-eye-chart:has(.radio-eye-chart__row:nth-child(2) input:checked) .radio-eye-chart__read > :nth-child(2), .radio-eye-chart:has(.radio-eye-chart__row:nth-child(3) input:checked) .radio-eye-chart__read > :nth-child(3), .radio-eye-chart:has(.radio-eye-chart__row:nth-child(4) input:checked) .radio-eye-chart__read > :nth-child(4), .radio-eye-chart:has(.radio-eye-chart__row:nth-child(5) input:checked) .radio-eye-chart__read > :nth-child(5) { opacity: 1; transform: none; transition-delay: 0.2s; }
@media (prefers-reduced-motion: reduce) {
.radio-eye-chart * { animation: none !important; transition: none !important; }
}Goal: Create a "Eye Chart" radio.
Visual behavior: A reading size picker on an optician chart: the pointer moves to the row you pick, which racks into sharp focus while every other row blurs by how far it is from it.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Each row's letters get a blur and an opacity from their distance to the chosen row, one :has() rule per pair, with a slow transition so the chart racks focus like a lens. Hovering a row brings it into focus too.
- The underline and the pointer's tip ride one linear() spring to the row, and a keyframe per row taps the pointer when it arrives.
Accessibility: A role="radiogroup" labelled "Reading size" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each row is a <label> whose input is named Huge, Large, Default, Small or Tiny, since the letters themselves are aria-hidden; focus outlines the row's letters. Under prefers-reduced-motion the focus changes at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Eye Chart" radio.
Visual behavior: A reading size picker on an optician chart: the pointer moves to the row you pick, which racks into sharp focus while every other row blurs by how far it is from it.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Each row's letters get a blur and an opacity from their distance to the chosen row, one :has() rule per pair, with a slow transition so the chart racks focus like a lens. Hovering a row brings it into focus too.
- The underline and the pointer's tip ride one linear() spring to the row, and a keyframe per row taps the pointer when it arrives.
Accessibility: A role="radiogroup" labelled "Reading size" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each row is a <label> whose input is named Huge, Large, Default, Small or Tiny, since the letters themselves are aria-hidden; focus outlines the row's letters. Under prefers-reduced-motion the focus changes at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A reading size picker on an optician chart: the pointer moves to the row you pick, which racks into sharp focus while every other row blurs by how far it is from it.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, spring, hover
- Techniques
- filter, css transition, css variables, spring
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" labelled "Reading size" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each row is a <label> whose input is named Huge, Large, Default, Small or Tiny, since the letters themselves are aria-hidden; focus outlines the row's letters. Under prefers-reduced-motion the focus changes at once.