Weight

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

const NAMES = ['Thin', 'Extra light', 'Light', 'Regular', 'Medium', 'Semibold', 'Bold', 'Extra bold', 'Black'];

const css = `
  .slider-weight { display: grid; justify-items: center; gap: 10px; width: 220px; }
  .slider-weight__sample { color: light-dark(#373a40, #f1f2f3); font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; font-size: 68px; line-height: 1; letter-spacing: -0.02em; font-weight: calc(100 + var(--slider-weight-v) * 800); transition: font-weight 0.2s; }
  .slider-weight__name { color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
  .slider-weight__name b { color: light-dark(#373a40, #f1f2f3); font-weight: 600; font-variant-numeric: tabular-nums; }
  .slider-weight__track { position: relative; width: 100%; height: 24px; }
  .slider-weight__rail { position: absolute; left: 0; right: 0; top: 7px; height: 10px; background: light-dark(#dbdde1, #2e3035); clip-path: polygon(0 45%, 100% 0, 100% 100%, 0 55%); }
  .slider-weight__thumb { position: absolute; top: 2px; left: calc(var(--slider-weight-v) * (100% - 20px)); width: 20px; height: 20px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 3px light-dark(#f3f4f7, #1a1c1f), 0 2px 6px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
  .slider-weight:has(.slider-weight__input:focus-visible) .slider-weight__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; }
  .slider-weight__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
  .slider-weight__input::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; }
  .slider-weight__input::-moz-range-thumb { width: 20px; height: 20px; border: 0; }
  @media (prefers-reduced-motion: reduce) {
    .slider-weight__sample { transition: none; }
  }
`;

export function WeightSlider() {
  const [w, setW] = useState(400);
  const v = (w - 100) / 800;
  return (
    <>
      <style>
        {css}
      </style>
      <div className="slider-weight" style={{ '--slider-weight-v': v } as React.CSSProperties}>
        <span className="slider-weight__sample" aria-hidden="true">
          Aa
        </span>
        <span className="slider-weight__name" aria-hidden="true">
          {NAMES[Math.round(v * 8)]} <b>{w}</b>
        </span>
        <div className="slider-weight__track">
          <span className="slider-weight__rail" />
          <span className="slider-weight__thumb" />
          <input
            className="slider-weight__input"
            type="range"
            min={100}
            max={900}
            step={10}
            value={w}
            onChange={(e) => setW(+e.target.value)}
            aria-label="Font weight"
            aria-valuetext={`${w}, ${NAMES[Math.round(v * 8)]}`}
          />
        </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-weight
Source on GitHub

About this component

A font-weight slider: the sample letters thicken from Thin to Black as you drag, and the weight name and number change below.

Dependencies
None
Difficulty
Beginner
Triggers
state change, morph
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 sample changes weight without a transition.

More sliders

See all
Window
Windsock
Zipper
Balance