Specimen

No dependencies
Code format
React + Tailwind
import { useId } from 'react';

const OPTIONS = [{"value": "serif", "label": "Serif"}, {"value": "sans", "label": "Sans"}, {"value": "mono", "label": "Mono"}];

const css = `
  .radio-specimen { --i: 0; display: grid; justify-items: center; gap: 12px; width: 260px; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(1) input:checked) { --i: 0; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(2) input:checked) { --i: 1; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(3) input:checked) { --i: 2; }
  .radio-specimen__sheet { display: grid; width: 220px; height: 112px; }
  .radio-specimen__face { grid-area: 1 / 1; display: grid; justify-items: center; align-content: center; gap: 4px; color: light-dark(#373a40, #f1f2f3); opacity: 0; filter: blur(4px); transform: translateY(8px) scale(0.96); transition: opacity 0.35s, filter 0.35s, transform 0.45s cubic-bezier(0.3, 1.3, 0.5, 1); }
  .radio-specimen__big { font-size: 56px; line-height: 1; letter-spacing: -0.02em; }
  .radio-specimen__small { color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(1) input:checked) .radio-specimen__face:nth-child(1) { opacity: 1; filter: none; transform: none; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(2) input:checked) .radio-specimen__face:nth-child(2) { opacity: 1; filter: none; transform: none; }
  .radio-specimen:has(.radio-specimen__opt:nth-child(3) input:checked) .radio-specimen__face:nth-child(3) { opacity: 1; filter: none; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .radio-specimen__face { transition: opacity 0.2s; filter: none; transform: none; }
  }
  .radio-specimen__opts { display: flex; gap: 3px; padding: 4px; border-radius: 12px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
  .radio-specimen__opt { position: relative; flex: 1; padding: 7px 10px; border-radius: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 12.5px; font-weight: 500; text-align: center; white-space: nowrap; cursor: pointer; transition: background-color 0.25s, color 0.25s; }
  .radio-specimen__opt:hover { color: light-dark(#42454c, #e5e6e9); }
  .radio-specimen__opt:has(input:checked) { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
  .radio-specimen__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
  .radio-specimen__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
`;

export function SpecimenRadio() {
  const name = useId();
  return (
    <>
      <style>
        {css}
      </style>
      <div className="radio-specimen">
        <div className="radio-specimen__sheet" aria-hidden="true">
          <span className="radio-specimen__face" style={{ fontFamily: "Georgia, 'Times New Roman', serif" }}>
            <span className="radio-specimen__big">Ag</span>
            <span className="radio-specimen__small">Quiet joy of type · Serif</span>
          </span>
          <span className="radio-specimen__face" style={{ fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif" }}>
            <span className="radio-specimen__big">Ag</span>
            <span className="radio-specimen__small">Quiet joy of type · Sans</span>
          </span>
          <span className="radio-specimen__face" style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }}>
            <span className="radio-specimen__big">Ag</span>
            <span className="radio-specimen__small">Quiet joy of type · Mono</span>
          </span>
        </div>
        <div className="radio-specimen__opts" role="radiogroup" aria-label="Typeface">
          {OPTIONS.map((o, i) => (
            <label key={o.value} className="radio-specimen__opt">
              <input className="radio-specimen__input" type="radio" name={name} value={o.value} defaultChecked={i === 0} />
              {o.label}
            </label>
          ))}
        </div>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/radio-specimen
Source on GitHub

About this component

A typeface picker: choose serif, sans or mono and the specimen dissolves and resolves in the new face, like a lens racking focus.

Dependencies
None
Difficulty
Beginner
Triggers
click, state change, focus
Techniques
filter, css transition, text animation
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A role="radiogroup" with an aria-label around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once; :focus-visible on the input outlines its option. Under prefers-reduced-motion the specimens cross-fade without blur.

More radios

See all
Altitude
Aperture
Bloom
Brush