Skip to content

Slider

A range input with a token-painted track, live value read-out and custom value formatting.

Formsstarterformrangenumericpricing

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

The exact file rendered in the preview above.

'use client'

import { useState } from 'react'
import { Field } from '@/components/ui/field'
import { Slider } from '@/components/ui/slider'
import { DemoColumn, DemoStage } from './_kit'

export default function SliderDemo() {
  const [seats, setSeats] = useState(24)

  return (
    <DemoStage>
      <DemoColumn width="lg">
        <Field name="seats" label="Seats" hint="Arrow keys adjust by one, Page Up/Down by ten.">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={1}
              max={100}
              value={seats}
              onValueChange={setSeats}
              format={(value) => String(value)}
            />
          )}
        </Field>

        <Field name="budget" label="Monthly budget alert">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={0}
              max={5000}
              step={100}
              defaultValue={1800}
              format={(value) => `$${value.toLocaleString('en-US')}`}
            />
          )}
        </Field>

        <Field name="retention" label="Log retention">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={7}
              max={90}
              step={7}
              defaultValue={30}
              disabled
              format={(value) => `${value}d`}
            />
          )}
        </Field>

        <p className="text-xs text-ink-muted">
          Estimated cost:{' '}
          <span className="font-mono text-ink">${(seats * 18).toLocaleString('en-US')}</span> per
          month
        </p>
      </DemoColumn>
    </DemoStage>
  )
}

components/demos/slider.tsx

'use client'

import { useState } from 'react'
import { Field } from '@/components/ui/field'
import { Slider } from '@/components/ui/slider'
import { DemoColumn, DemoStage } from './_kit'

export default function SliderDemo() {
  const [seats, setSeats] = useState(24)

  return (
    <DemoStage>
      <DemoColumn width="lg">
        <Field name="seats" label="Seats" hint="Arrow keys adjust by one, Page Up/Down by ten.">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={1}
              max={100}
              value={seats}
              onValueChange={setSeats}
              format={(value) => String(value)}
            />
          )}
        </Field>

        <Field name="budget" label="Monthly budget alert">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={0}
              max={5000}
              step={100}
              defaultValue={1800}
              format={(value) => `$${value.toLocaleString('en-US')}`}
            />
          )}
        </Field>

        <Field name="retention" label="Log retention">
          {(field) => (
            <Slider
              id={field.id}
              name={field.name}
              aria-describedby={field['aria-describedby']}
              min={7}
              max={90}
              step={7}
              defaultValue={30}
              disabled
              format={(value) => `${value}d`}
            />
          )}
        </Field>

        <p className="text-xs text-ink-muted">
          Estimated cost:{' '}
          <span className="font-mono text-ink">${(seats * 18).toLocaleString('en-US')}</span> per
          month
        </p>
      </DemoColumn>
    </DemoStage>
  )
}

components/ui/slider.tsx

'use client'

import { useState, type InputHTMLAttributes } from 'react'
import { cn } from '@/lib/cn'

/**
 * Slider
 *
 * A native `range` input with a token-styled track and thumb. Native means
 * arrow keys, Home/End, Page Up/Down and `aria-valuetext` all work without a
 * single key handler of our own.
 *
 * The filled portion of the track is painted with a gradient driven by the
 * current value, which avoids a second absolutely-positioned element that
 * would need to stay in sync.
 */
export interface SliderProps
  extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'value' | 'defaultValue'> {
  min?: number
  max?: number
  step?: number
  defaultValue?: number
  value?: number
  /** Renders the live value on the trailing edge. */
  showValue?: boolean
  /** Formats the displayed value and `aria-valuetext`. */
  format?: (value: number) => string
  onValueChange?: (value: number) => void
}

export function Slider({
  min = 0,
  max = 100,
  step = 1,
  defaultValue,
  value: controlledValue,
  showValue = true,
  format = (v) => String(v),
  onValueChange,
  className,
  disabled,
  ...props
}: SliderProps) {
  const [uncontrolled, setUncontrolled] = useState(defaultValue ?? min)
  const value = controlledValue ?? uncontrolled
  const percent = max === min ? 0 : ((value - min) / (max - min)) * 100

  return (
    <div className="flex items-center gap-3">
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        disabled={disabled}
        aria-valuetext={format(value)}
        onChange={(event) => {
          const next = Number(event.target.value)
          if (controlledValue === undefined) setUncontrolled(next)
          onValueChange?.(next)
        }}
        className={cn('foundry-slider', className)}
        style={{
          // The filled portion of the track is painted from this one variable,
          // so fill and thumb can never disagree about the current value.
          ['--slider-fill' as string]: `${percent}%`,
        }}
        {...props}
      />
      {showValue ? (
        <span className="w-12 shrink-0 text-right font-mono text-xs text-ink-muted tabular-nums">
          {format(value)}
        </span>
      ) : null}
    </div>
  )
}

app/globals.css

@import 'tailwindcss';

/* ==========================================================================
   FOUNDRY DESIGN TOKENS
   --------------------------------------------------------------------------
   Every visual decision in Foundry resolves to a CSS custom property declared
   in this file. Three orthogonal axes compose the final look:

     1. scheme   -> [data-theme="light" | "dark"]      (surface + ink)
     2. palette  -> [data-palette="editorial" | ...]   (accent, type, shape)
     3. density  -> [data-density="compact" | ...]     (rhythm, control size)

   Selectors are attribute-based rather than :root-only so any subtree — a
   component preview frame, a starter shell — can opt into its own theme.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. SCHEME — light (default)
   -------------------------------------------------------------------------- */
:root,
[data-theme='light'] {
  color-scheme: light;

  /* Surfaces: warm off-white paper stock rising to pure white cards. */
  --color-canvas: #faf8f3;
  --color-surface: #ffffff;
  --color-surface-raised: #ffffff;
  --color-surface-sunken: #f2efe7;
  --color-surface-inverse: #17171a;
  --color-code-surface: #16161a;
  --color-code-gutter: #1e1e24;

  /* Ink: graphite rather than pure black. */
  --color-ink: #17171a;
  --color-ink-strong: #000000;
  --color-ink-muted: #63615c;
  --color-ink-subtle: #8c8981;
  --color-ink-inverse: #f7f6f2;
  --color-code-ink: #e6e6ec;

  /* Lines: restrained, warm. */
  --color-border: #e4dfd4;
  --color-border-strong: #cfc9bb;
  --color-border-subtle: #eee9df;

  /* Accent: electric indigo. */
  --color-accent: #3b41d8;
  --color-accent-hover: #3036c2;
  --color-accent-active: #262ba6;
  --color-accent-ink: #ffffff;
  --color-accent-soft: #ecedfd;
  --color-accent-soft-ink: #2c31ad;
  --color-accent-line: #c3c6f6;

  /* Status. Each carries a surface, an ink and a line so status never relies
     on hue alone — icon + label always accompany the colour. */
  --color-success: #197a4b;
  --color-success-soft: #e6f5ec;
  --color-success-line: #b3ddc6;
  --color-warning: #8a5a06;
  --color-warning-soft: #fdf2dd;
  --color-warning-line: #ecd39a;
  --color-danger: #b4231f;
  --color-danger-soft: #fdeceb;
  --color-danger-line: #f2c0bd;
  --color-info: #1a5f95;
  --color-info-soft: #e8f2fb;
  --color-info-line: #b6d5ee;

  /* Elevation: paper-like, never glowing. */
  --shadow-xs: 0 1px 1px rgb(23 23 26 / 0.04);
  --shadow-sm: 0 1px 2px rgb(23 23 26 / 0.06), 0 1px 1px rgb(23 23 26 / 0.04);
  --shadow-md: 0 4px 10px -2px rgb(23 23 26 / 0.08), 0 2px 4px -2px rgb(23 23 26 / 0.05);
  --shadow-lg: 0 12px 28px -8px rgb(23 23 26 / 0.14), 0 4px 10px -4px rgb(23 23 26 / 0.06);
  --shadow-overlay: 0 24px 60px -18px rgb(23 23 26 / 0.28), 0 8px 20px -10px rgb(23 23 26 / 0.12);

  --color-scrim: rgb(23 23 26 / 0.42);
  --color-grid-line: rgb(23 23 26 / 0.05);
}

/* --------------------------------------------------------------------------
   1b. SCHEME — dark (authored, not inverted)
   -------------------------------------------------------------------------- */
[data-theme='dark'] {
  color-scheme: dark;

  --color-canvas: #101012;
  --color-surface: #171719;
  --color-surface-raised: #1d1d21;
  --color-surface-sunken: #0b0b0d;
  --color-surface-inverse: #f7f6f2;
  --color-code-surface: #0d0d0f;
  --color-code-gutter: #141417;

  --color-ink: #ecebe7;
  --color-ink-strong: #ffffff;
  --color-ink-muted: #a3a19b;
  --color-ink-subtle: #7d7b75;
  --color-ink-inverse: #17171a;
  --color-code-ink: #e6e6ec;

  --color-border: #2a2a2f;
  --color-border-strong: #3d3d44;
  --color-border-subtle: #202024;

  --color-accent: #7f87ff;
  --color-accent-hover: #949bff;
  --color-accent-active: #a8adff;
  --color-accent-ink: #101024;
  --color-accent-soft: #1c1d3a;
  --color-accent-soft-ink: #b2b7ff;
  --color-accent-line: #343670;

  --color-success: #4ec98a;
  --color-success-soft: #10261b;
  --color-success-line: #245c40;
  --color-warning: #e0ab52;
  --color-warning-soft: #2a1f0c;
  --color-warning-line: #5e4718;
  --color-danger: #f4837c;
  --color-danger-soft: #2c1413;
  --color-danger-line: #66302d;
  --color-info: #6fb2e8;
  --color-info-soft: #0f2130;
  --color-info-line: #244a68;

  --shadow-xs: 0 1px 1px rgb(0 0 0 / 0.3);
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4), 0 1px 1px rgb(0 0 0 / 0.3);
  --shadow-md: 0 4px 10px -2px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.4);
  --shadow-lg: 0 12px 28px -8px rgb(0 0 0 / 0.6), 0 4px 10px -4px rgb(0 0 0 / 0.4);
  --shadow-overlay: 0 24px 60px -18px rgb(0 0 0 / 0.75), 0 8px 20px -10px rgb(0 0 0 / 0.5);

  --color-scrim: rgb(0 0 0 / 0.66);
  --color-grid-line: rgb(255 255 255 / 0.05);
}

/* --------------------------------------------------------------------------
   2. SHARED NON-SCHEME TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* Typography scale — a modular 1.2 ratio anchored at 16px. */
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --font-sans:
    ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

  --text-2xs: 0.6875rem;
  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 0.9375rem;
  --text-md: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: 2.125rem;
  --text-4xl: 2.75rem;
  --text-5xl: 3.5rem;
  --text-6xl: 4.5rem;

  --leading-tight: 1.12;
  --leading-snug: 1.28;
  --leading-normal: 1.6;
  --leading-relaxed: 1.75;

  --tracking-tighter: -0.03em;
  --tracking-tight: -0.015em;
  --tracking-normal: 0em;
  --tracking-wide: 0.04em;
  --tracking-label: 0.11em;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Spacing — 4px base grid. */
  --space-0: 0px;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* Radius — deliberately restrained. */
  --radius-none: 0px;
  --radius-xs: 2px;
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --radius-xl: 14px;
  --radius-full: 9999px;

  /* Borders */
  --border-width: 1px;
  --border-width-strong: 2px;

  /* Motion */
  --duration-instant: 80ms;
  --duration-fast: 140ms;
  --duration-normal: 220ms;
  --duration-slow: 360ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-entrance: cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-exit: cubic-bezier(0.3, 0, 0.8, 0.15);

  /* Focus */
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;
  --focus-ring-color: var(--color-accent);

  /* Layout constants */
  --layout-header-height: 3.5rem;
  --layout-sidebar-width: 16.5rem;
  --layout-toc-width: 14rem;
  --layout-content-max: 78rem;
  --layout-prose-max: 46rem;

  /* Breakpoints (documented + consumed by the tokens page) */
  --breakpoint-sm: 40rem;
  --breakpoint-md: 48rem;
  --breakpoint-lg: 64rem;
  --breakpoint-xl: 80rem;
  --breakpoint-2xl: 96rem;

  /* Density (default) */
  --density-control-height: 2.25rem;
  --density-control-height-sm: 1.875rem;
  --density-control-height-lg: 2.75rem;
  --density-control-padding-x: 0.875rem;
  --density-gap: 0.75rem;
  --density-stack: 1rem;
  --density-section-y: 5rem;
  --density-card-padding: 1.25rem;
  --density-row-height: 2.75rem;
}

/* --------------------------------------------------------------------------
   3. PALETTES — visible personality shifts, not hue-only tweaks

   Each palette has a light form and a dark form. The dark form is matched two
   ways on purpose: the compound selector covers a palette set on the same
   element as the scheme (the page root, a preview iframe's document), and the
   descendant selector covers a palette scoped to a subtree while the scheme
   stays on <html> — which is exactly what a starter shell does. Without the
   second form a starter would keep its light surfaces under dark ink.
   -------------------------------------------------------------------------- */
[data-palette='editorial'] {
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 6px;
  --radius-xl: 8px;
  --tracking-display: -0.02em;
}

[data-palette='corporate'] {
  --font-display: var(--font-sans);
  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 6px;
  --radius-xl: 8px;
  --tracking-display: -0.01em;
  --color-accent: #17558f;
  --color-accent-hover: #12466f;
  --color-accent-active: #0d3a5d;
  --color-accent-soft: #e7f0f8;
  --color-accent-soft-ink: #124168;
  --color-accent-line: #b3cee6;
  --color-canvas: #f6f7f9;
  --color-surface-sunken: #eceff3;
  --color-border: #dde2e9;
  --color-border-strong: #c3cbd6;
  --color-border-subtle: #e8ebf0;
}

[data-theme='dark'][data-palette='corporate'],
[data-theme='dark'] [data-palette='corporate'] {
  --color-accent: #6aaae4;
  --color-accent-hover: #83bbef;
  --color-accent-active: #9ac8f3;
  --color-accent-ink: #06192a;
  --color-accent-soft: #10222f;
  --color-accent-soft-ink: #9ccbf0;
  --color-accent-line: #1f4258;
  --color-canvas: #0d1013;
  --color-surface: #14181c;
  --color-surface-raised: #1a1f24;
  --color-surface-sunken: #080b0e;
  --color-border: #242b31;
  --color-border-strong: #364049;
  --color-border-subtle: #1c2126;
}

[data-palette='minimal'] {
  --font-display: var(--font-sans);
  --radius-xs: 0px;
  --radius-sm: 0px;
  --radius-md: 0px;
  --radius-lg: 0px;
  --radius-xl: 0px;
  --tracking-display: -0.025em;
  --color-accent: #17171a;
  --color-accent-hover: #000000;
  --color-accent-active: #000000;
  --color-accent-ink: #ffffff;
  --color-accent-soft: #f0efec;
  --color-accent-soft-ink: #17171a;
  --color-accent-line: #d6d3cc;
  --color-canvas: #ffffff;
  --color-surface-sunken: #f6f5f2;
  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: 0 1px 0 var(--color-border);
}

[data-theme='dark'][data-palette='minimal'],
[data-theme='dark'] [data-palette='minimal'] {
  --color-accent: #f2f1ed;
  --color-accent-hover: #ffffff;
  --color-accent-active: #ffffff;
  --color-accent-ink: #101012;
  --color-accent-soft: #202024;
  --color-accent-soft-ink: #f2f1ed;
  --color-accent-line: #35353b;
  --color-canvas: #0a0a0b;
  --color-surface: #131315;
  --color-surface-sunken: #050506;
}

[data-palette='modern'] {
  --font-display: var(--font-sans);
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  --tracking-display: -0.035em;
  --color-accent: #0a7a6a;
  --color-accent-hover: #086356;
  --color-accent-active: #065347;
  --color-accent-soft: #e2f4f0;
  --color-accent-soft-ink: #06584c;
  --color-accent-line: #a8ded5;
  --color-canvas: #f7f8f8;
  --color-surface-sunken: #edf0ef;
  --color-border: #e0e5e4;
  --color-border-strong: #c6cecc;
}

[data-theme='dark'][data-palette='modern'],
[data-theme='dark'] [data-palette='modern'] {
  --color-accent: #43d5bb;
  --color-accent-hover: #5fe0c9;
  --color-accent-active: #7ae8d4;
  --color-accent-ink: #04201b;
  --color-accent-soft: #0c2a25;
  --color-accent-soft-ink: #6fdcc6;
  --color-accent-line: #17564a;
  --color-canvas: #0a0f0e;
  --color-surface: #111817;
  --color-surface-raised: #16201e;
  --color-surface-sunken: #060a09;
  --color-border: #1f2b29;
  --color-border-strong: #2f403d;
}

[data-palette='warm'] {
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --radius-sm: 5px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 18px;
  --tracking-display: -0.018em;
  --color-accent: #b04a1c;
  --color-accent-hover: #963d15;
  --color-accent-active: #7d3210;
  --color-accent-soft: #fbeade;
  --color-accent-soft-ink: #8f3a13;
  --color-accent-line: #eec4a6;
  --color-canvas: #fbf6ef;
  --color-surface: #fffdfa;
  --color-surface-sunken: #f4ebdf;
  --color-border: #e8dbc9;
  --color-border-strong: #d3c0a7;
  --color-border-subtle: #f0e6d8;
}

[data-theme='dark'][data-palette='warm'],
[data-theme='dark'] [data-palette='warm'] {
  --color-accent: #f09256;
  --color-accent-hover: #f5a470;
  --color-accent-active: #f8b689;
  --color-accent-ink: #2a1206;
  --color-accent-soft: #2c1a0e;
  --color-accent-soft-ink: #f0a778;
  --color-accent-line: #5c3419;
  --color-canvas: #14100c;
  --color-surface: #1b1611;
  --color-surface-raised: #221c16;
  --color-surface-sunken: #0e0b08;
  --color-border: #2f2720;
  --color-border-strong: #443830;
  --color-border-subtle: #241e18;
}

/* --------------------------------------------------------------------------
   4. DENSITY
   -------------------------------------------------------------------------- */
[data-density='compact'] {
  --density-control-height: 1.875rem;
  --density-control-height-sm: 1.625rem;
  --density-control-height-lg: 2.25rem;
  --density-control-padding-x: 0.625rem;
  --density-gap: 0.5rem;
  --density-stack: 0.625rem;
  --density-section-y: 3rem;
  --density-card-padding: 0.875rem;
  --density-row-height: 2.125rem;
}

[data-density='relaxed'] {
  --density-control-height: 2.625rem;
  --density-control-height-sm: 2.25rem;
  --density-control-height-lg: 3.125rem;
  --density-control-padding-x: 1.125rem;
  --density-gap: 1rem;
  --density-stack: 1.5rem;
  --density-section-y: 7rem;
  --density-card-padding: 1.75rem;
  --density-row-height: 3.25rem;
}

/* --------------------------------------------------------------------------
   5. TAILWIND THEME BRIDGE
   Every token above becomes a first-class Tailwind utility.
   -------------------------------------------------------------------------- */
@theme inline {
  --color-canvas: var(--color-canvas);
  --color-surface: var(--color-surface);
  --color-surface-raised: var(--color-surface-raised);
  --color-surface-sunken: var(--color-surface-sunken);
  --color-surface-inverse: var(--color-surface-inverse);
  --color-code-surface: var(--color-code-surface);
  --color-code-gutter: var(--color-code-gutter);
  --color-code-ink: var(--color-code-ink);

  --color-ink: var(--color-ink);
  --color-ink-strong: var(--color-ink-strong);
  --color-ink-muted: var(--color-ink-muted);
  --color-ink-subtle: var(--color-ink-subtle);
  --color-ink-inverse: var(--color-ink-inverse);

  --color-line: var(--color-border);
  --color-line-strong: var(--color-border-strong);
  --color-line-subtle: var(--color-border-subtle);

  --color-accent: var(--color-accent);
  --color-accent-hover: var(--color-accent-hover);
  --color-accent-active: var(--color-accent-active);
  --color-accent-ink: var(--color-accent-ink);
  --color-accent-soft: var(--color-accent-soft);
  --color-accent-soft-ink: var(--color-accent-soft-ink);
  --color-accent-line: var(--color-accent-line);

  --color-success: var(--color-success);
  --color-success-soft: var(--color-success-soft);
  --color-success-line: var(--color-success-line);
  --color-warning: var(--color-warning);
  --color-warning-soft: var(--color-warning-soft);
  --color-warning-line: var(--color-warning-line);
  --color-danger: var(--color-danger);
  --color-danger-soft: var(--color-danger-soft);
  --color-danger-line: var(--color-danger-line);
  --color-info: var(--color-info);
  --color-info-soft: var(--color-info-soft);
  --color-info-line: var(--color-info-line);

  --color-scrim: var(--color-scrim);
  --color-grid-line: var(--color-grid-line);

  --font-display: var(--font-display);
  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);

  --text-2xs: var(--text-2xs);
  --text-xs: var(--text-xs);
  --text-sm: var(--text-sm);
  --text-base: var(--text-base);
  --text-md: var(--text-md);
  --text-lg: var(--text-lg);
  --text-xl: var(--text-xl);
  --text-2xl: var(--text-2xl);
  --text-3xl: var(--text-3xl);
  --text-4xl: var(--text-4xl);
  --text-5xl: var(--text-5xl);
  --text-6xl: var(--text-6xl);

  --radius-xs: var(--radius-xs);
  --radius-sm: var(--radius-sm);
  --radius-md: var(--radius-md);
  --radius-lg: var(--radius-lg);
  --radius-xl: var(--radius-xl);
  --radius-full: var(--radius-full);

  --shadow-xs: var(--shadow-xs);
  --shadow-sm: var(--shadow-sm);
  --shadow-md: var(--shadow-md);
  --shadow-lg: var(--shadow-lg);
  --shadow-overlay: var(--shadow-overlay);

  --spacing-control: var(--density-control-height);
  --spacing-control-sm: var(--density-control-height-sm);
  --spacing-control-lg: var(--density-control-height-lg);
  --spacing-gap: var(--density-gap);
  --spacing-stack: var(--density-stack);
  --spacing-section: var(--density-section-y);
  --spacing-card: var(--density-card-padding);
  --spacing-row: var(--density-row-height);

  --breakpoint-sm: 40rem;
  --breakpoint-md: 48rem;
  --breakpoint-lg: 64rem;
  --breakpoint-xl: 80rem;
  --breakpoint-2xl: 96rem;

  --ease-standard: var(--ease-standard);
  --ease-entrance: var(--ease-entrance);
  --ease-exit: var(--ease-exit);
}

/* --------------------------------------------------------------------------
   6. BASE LAYER
   -------------------------------------------------------------------------- */
@layer base {
  *,
  *::before,
  *::after {
    border-color: var(--color-border);
  }

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--layout-header-height) + 1.5rem);
  }

  body {
    background-color: var(--color-canvas);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    color: var(--color-ink-strong);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
  }

  p {
    text-wrap: pretty;
  }

  code,
  kbd,
  samp,
  pre {
    font-family: var(--font-mono);
    font-size: 0.92em;
  }

  :focus-visible {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
    border-radius: var(--radius-xs);
  }

  ::selection {
    background: var(--color-accent-soft);
    color: var(--color-accent-soft-ink);
  }

  /* Long identifiers (component slugs, tokens) must never blow out a layout. */
  .break-token {
    overflow-wrap: anywhere;
    word-break: break-word;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   7. UTILITIES
   -------------------------------------------------------------------------- */
@utility label-caps {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-weight: var(--weight-medium);
}

@utility display-type {
  font-family: var(--font-display);
  letter-spacing: var(--tracking-display, -0.02em);
}

@utility grid-paper {
  background-image:
    linear-gradient(to right, var(--color-grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--color-grid-line) 1px, transparent 1px);
  background-size: 2rem 2rem;
}

@utility hide-scrollbar {
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

@utility thin-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border-strong) transparent;
}

/* Animation primitives used by overlays, toasts and skeletons. */
@keyframes foundry-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes foundry-scale-in {
  from {
    opacity: 0;
    transform: translateY(4px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes foundry-slide-right {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes foundry-slide-left {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes foundry-slide-up {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes foundry-spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes foundry-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

@keyframes foundry-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@utility animate-fade-in {
  animation: foundry-fade-in var(--duration-fast) var(--ease-entrance);
}

@utility animate-scale-in {
  animation: foundry-scale-in var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-right {
  animation: foundry-slide-right var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-left {
  animation: foundry-slide-left var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-up {
  animation: foundry-slide-up var(--duration-normal) var(--ease-entrance);
}

@utility animate-spin-token {
  animation: foundry-spin 0.7s linear infinite;
}

@utility animate-pulse-token {
  animation: foundry-pulse 1.6s var(--ease-standard) infinite;
}

@utility animate-marquee {
  animation: foundry-marquee 40s linear infinite;
}

/* --------------------------------------------------------------------------
   8. PROSE — documentation typography, token driven
   -------------------------------------------------------------------------- */
.prose-foundry {
  color: var(--color-ink);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: var(--layout-prose-max);
}

.prose-foundry > * + * {
  margin-top: var(--space-4);
}

.prose-foundry h2 {
  font-size: var(--text-xl);
  margin-top: var(--space-10);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
}

.prose-foundry h3 {
  font-size: var(--text-lg);
  margin-top: var(--space-8);
  margin-bottom: var(--space-2);
}

.prose-foundry h4 {
  font-size: var(--text-md);
  margin-top: var(--space-6);
  margin-bottom: var(--space-2);
}

.prose-foundry ul,
.prose-foundry ol {
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.prose-foundry ul {
  list-style: disc;
}

.prose-foundry ol {
  list-style: decimal;
}

.prose-foundry li::marker {
  color: var(--color-ink-subtle);
}

.prose-foundry a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

.prose-foundry a:hover {
  color: var(--color-accent-hover);
}

.prose-foundry strong {
  font-weight: var(--weight-semibold);
  color: var(--color-ink-strong);
}

.prose-foundry :not(pre) > code {
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-xs);
  padding: 0.1em 0.35em;
  color: var(--color-ink);
}

.prose-foundry blockquote {
  border-left: 2px solid var(--color-accent-line);
  padding-left: var(--space-4);
  color: var(--color-ink-muted);
  font-style: italic;
}

/* --------------------------------------------------------------------------
   9. RANGE INPUT
   Vendor pseudo-elements cannot be expressed as utilities, so the slider's
   track and thumb are authored here and driven by --slider-fill.
   -------------------------------------------------------------------------- */
.foundry-slider {
  width: 100%;
  min-width: 0;
  height: 1.25rem;
  cursor: pointer;
  appearance: none;
  background: transparent;
}

.foundry-slider:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.foundry-slider::-webkit-slider-runnable-track {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: linear-gradient(
    to right,
    var(--color-accent) var(--slider-fill, 0%),
    var(--color-surface-sunken) var(--slider-fill, 0%)
  );
}

.foundry-slider::-webkit-slider-thumb {
  appearance: none;
  margin-top: -0.3125rem;
  height: 1rem;
  width: 1rem;
  border-radius: var(--radius-full);
  border: 2px solid var(--color-accent);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.foundry-slider::-moz-range-track {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--color-surface-sunken);
}

.foundry-slider::-moz-range-progress {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--color-accent);
}

.foundry-slider::-moz-range-thumb {
  height: 1rem;
  width: 1rem;
  border-radius: var(--radius-full);
  border: 2px solid var(--color-accent);
  background: var(--color-surface);
}

/* ---------------------------------------------------------------------------
   Progressive enhancement
   ---------------------------------------------------------------------------
   The blocking script in <head> stamps data-js="on" before first paint, so
   these swaps never flash. Controls that only work with scripting are hidden
   when there is none, and a link-based fallback takes their place — the shell
   must never present a control that cannot do anything.
--------------------------------------------------------------------------- */
[data-js='on'] [data-nojs] {
  display: none !important;
}

html:not([data-js='on']) [data-js-only] {
  display: none !important;
}

/* The code viewer's collapse is presentational: the full listing is always in
   the DOM, so with no scripting to expand it the clamp simply does not apply. */
[data-js='on'] [data-code-body][data-collapsed='true'] {
  max-height: calc(var(--code-collapsed-lines) * 1.21875rem + 1.75rem);
  overflow-y: hidden;
}

/* Same idea for the mobile documentation outline. */
[data-js='on'] [data-collapsed='true']#docs-outline {
  display: none;
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

For approximate numeric input where the relationship between value and range matters more than precision — seat counts, budgets, retention windows.

  • Always show the current value; a slider without a read-out is a guess.
  • Pair with a number input when an exact figure is required.
  • The filled portion of the track is painted from a single CSS variable, so fill and thumb can never drift apart.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Continuous
  • Stepped
  • Formatted value — currency, duration
  • Disabled

Accessibility

Native range
Arrow keys, Home, End and Page Up/Down all work without a single key handler.
Value text
`aria-valuetext` carries the formatted value, so "$1,800" is announced rather than "1800".

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.