Tracking

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

const WORD = 'MOTION';

const css = `
  .slider-tracking { display: grid; justify-items: center; gap: 14px; width: 230px; }
  .slider-tracking__word { display: flex; justify-content: center; height: 40px; color: light-dark(#373a40, #f1f2f3); font-size: 28px; font-weight: 600; line-height: 40px; }
  .slider-tracking__char { display: inline-block; margin-right: calc((var(--slider-tracking-v) * 0.55 - 0.05) * 1em); transition: margin-right 0.45s cubic-bezier(0.3, 1.4, 0.5, 1); transition-delay: calc(var(--i) * 35ms); }
  .slider-tracking__char:last-child { margin-right: 0; }
  .slider-tracking__val { color: light-dark(#5f626a, #a7abb2); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; font-variant-numeric: tabular-nums; }
  .slider-tracking__track { position: relative; width: 100%; height: 24px; }
  .slider-tracking__rail { position: absolute; left: 0; right: 0; top: 11px; height: 2px; background: light-dark(#dbdde1, #2e3035); }
  .slider-tracking__rail::before, .slider-tracking__rail::after { content: ''; position: absolute; top: -4px; width: 2px; height: 10px; background: light-dark(#6d7179, #7d8087); }
  .slider-tracking__rail::before { left: 0; }
  .slider-tracking__rail::after { right: 0; }
  .slider-tracking__thumb { position: absolute; top: 2px; left: calc(var(--slider-tracking-v) * (100% - 24px)); width: 24px; height: 20px; border-radius: 5px; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
  .slider-tracking__thumb::before, .slider-tracking__thumb::after { content: ''; position: absolute; top: 6px; border: 4px solid transparent; }
  .slider-tracking__thumb::before { left: 1px; border-right-color: light-dark(#dbdde1, #2e3035); }
  .slider-tracking__thumb::after { right: 1px; border-left-color: light-dark(#dbdde1, #2e3035); }
  .slider-tracking:has(.slider-tracking__input:focus-visible) .slider-tracking__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
  .slider-tracking__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
  .slider-tracking__input::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 20px; }
  .slider-tracking__input::-moz-range-thumb { width: 24px; height: 20px; border: 0; }
  @media (prefers-reduced-motion: reduce) {
    .slider-tracking__char { transition: none; }
  }
`;

export function TrackingSlider() {
  const [v, setV] = useState(30);
  const em = ((v / 100) * 0.55 - 0.05).toFixed(2);
  return (
    <>
      <style>
        {css}
      </style>
      <div className="slider-tracking" style={{ '--slider-tracking-v': v / 100 } as React.CSSProperties}>
        <span className="slider-tracking__word" aria-hidden="true">
          {WORD.split('').map((c, i) => (
            <span key={i} className="slider-tracking__char" style={{ '--i': i } as React.CSSProperties}>
              {c}
            </span>
          ))}
        </span>
        <span className="slider-tracking__val" aria-hidden="true">
          {em} em
        </span>
        <div className="slider-tracking__track">
          <span className="slider-tracking__rail" />
          <span className="slider-tracking__thumb" />
          <input className="slider-tracking__input" type="range" min={0} max={100} value={v} onChange={(e) => setV(+e.target.value)} aria-label="Letter spacing" aria-valuetext={`${em} em`} />
        </div>
      </div>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/slider-tracking
Source on GitHub

About this component

A letter-spacing slider: the letters of the word spread apart and draw together as you drag, each a beat after the one before, with the value in em below.

Dependencies
None
Difficulty
Beginner
Triggers
state change, delay stagger
Techniques
css variables, css transition
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the letters move without easing.

More sliders

See all
Trail
Weight
Window
Windsock