Slider
A range input with a token-painted track, live value read-out and custom value formatting.
Live preview
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.
Related
All componentsInput
Single-line text entry with leading and trailing slots, three sizes and a shared control surface used by every other text control.
starter7 variantsField
The accessibility contract shared by every form control — label, help text, error, success and the ARIA wiring that connects them.
intermediateFeatured7 variantsProgress
Determinate and indeterminate progress as a bar or a ring, with four tones.
starter5 variants