# Barefoot — Theming

The whole framework is variables. Change a variable, everything that
depends on it follows. There is no build step, no Sass, no recompile.

> **v2.0:** The token API is frozen. See [api.md](api.md) for the
> stability contract and deprecation policy.

## Token reference

Auto-generated from [`src/tokens.css`](../src/tokens.css) — the source
of truth. Change a token there (its trailing comment becomes the
Purpose column) and run `npm run docs:tokens`, which regenerates the
tables between the markers below; `npm run check` does this on every
run, and a parity test fails CI if a new token skips it.

Color tokens are `light-dark()` pairs — *light value / dark value*,
resolved from the OS color scheme ([how](#how-lightdark-works-the-trick)).
Alpha-ramp tokens derive from base tokens via `color-mix()`: override
the base token and every ramp follows.

<!-- TOKENS:START -->
<!-- Generated by build/token-docs.mjs from src/tokens.css.
     Do not edit inside this region — change the token (its
     trailing comment is the Purpose column) and re-run
     npm run docs:tokens. -->

### Typography

| Token | Default | Purpose |
|---|---|---|
| `--bf-font` | `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` | body font |
| `--bf-font-mono` | `ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace` | code font |
| `--bf-line-height` | `1.6` | body leading |
| `--bf-font-weight-normal` | `400` | body, divider labels |
| `--bf-font-weight-medium` | `500` | buttons, labels, summaries, tabs |
| `--bf-font-weight-semibold` | `600` | headings, card/dialog headers, th, current states |
| `--bf-font-weight-bold` | `700` | .bf-brand |
| `--bf-letter-spacing-tight` | `-0.01em` | display/brand tracking |
| `--bf-letter-spacing-wide` | `0.05em` | uppercase labels (badge, th) |
| `--bf-letter-spacing-wider` | `0.08em` | overlines |
| `--bf-type-xs` | `0.75rem` | tiny text (footnotes) |
| `--bf-type-sm` | `0.875rem` | small text (figcaptions, hints) |
| `--bf-type-base` | `1rem` | body text |
| `--bf-type-md` | `clamp(1rem, 0.9375rem + 0.3125vw, 1.125rem)` | h4 |
| `--bf-type-lg` | `clamp(1.125rem, 1rem + 0.625vw, 1.375rem)` | h3 |
| `--bf-type-xl` | `clamp(1.375rem, 1.125rem + 1.25vw, 1.875rem)` | h2 |
| `--bf-type-2xl` | `clamp(1.75rem, 1.375rem + 1.875vw, 2.5rem)` | h1 |

### Spacing scale (1rem = 16px base)

| Token | Default | Purpose |
|---|---|---|
| `--bf-space-1` | `0.25rem` |  |
| `--bf-space-2` | `0.5rem` |  |
| `--bf-space-3` | `0.75rem` |  |
| `--bf-space-4` | `1rem` |  |
| `--bf-space-5` | `1.5rem` |  |
| `--bf-space-6` | `2rem` |  |
| `--bf-space-7` | `3rem` |  |
| `--bf-space-8` | `4rem` |  |

### Radii (small, neutral — no "rounded identity")

| Token | Default | Purpose |
|---|---|---|
| `--bf-radius` | `0.375rem` | component corners |
| `--bf-radius-sm` | `0.25rem` | small corners |
| `--bf-radius-lg` | `0.625rem` | large corners |
| `--bf-radius-full` | `999px` | pills — badges, switches, sliders, progress |

### Strokes (v2.2)

| Token | Default | Purpose |
|---|---|---|
| `--bf-border-width` | `1px` | hairline stroke for every component border |

### Color

| Token | Default | Purpose |
|---|---|---|
| `--bf-primary` | `light-dark(#1a1a1a, #ececec)` | the accent — change this and every component follows |
| `--bf-primary-fg` | `light-dark(#ffffff, #141414)` | text/icon on the accent |
| `--bf-surface` | `light-dark(#ffffff, #161616)` | page & component background |
| `--bf-surface-alt` | `light-dark(#f4f4f4, #202020)` | hover chips, code, pre |
| `--bf-text` | `light-dark(#1a1a1a, #e8e8e8)` | body text |
| `--bf-muted` | `light-dark(#5a5a5a, #a0a0a0)` | secondary text |
| `--bf-border` | `light-dark(#d8d8d8, #3a3a3a)` | hairlines, borders |
| `--bf-danger` | `light-dark(#b3261e, #f2b8b5)` | errors, destructive |
| `--bf-danger-fg` | `light-dark(#ffffff, #201000)` | text/icon on the danger fill |
| `--bf-success` | `light-dark(#1a7f37, #7ee2a8)` | success states, valid fields |
| `--bf-success-fg` | `light-dark(#ffffff, #0a2a16)` | text/icon on the success fill |
| `--bf-info` | `light-dark(#0969da, #8ac7ff)` | informational states |
| `--bf-info-fg` | `light-dark(#ffffff, #0b1f38)` | text/icon on the info fill |
| `--bf-warning` | `light-dark(#9a6700, #e3b341)` | warnings, caution |
| `--bf-warning-fg` | `light-dark(#ffffff, #2a1f00)` | text/icon on the warning fill |
| `--bf-focus-ring` | `light-dark(#1a1a1a, #e8e8e8)` | focus outline color |

### Backdrop

| Token | Default | Purpose |
|---|---|---|
| `--bf-backdrop` | `light-dark(rgb(0 0 0 / 0.5), rgb(0 0 0 / 0.6))` | dialog/modal backdrop overlay |

### Effects (default: no shadows — neutral by default)

| Token | Default | Purpose |
|---|---|---|
| `--bf-shadow` | `none` | default shadow (neutral by default) |
| `--bf-shadow-sm` | `0 1px 2px rgb(0 0 0 / 0.25)` | small shadow (thumbs, subtle depth) |
| `--bf-shadow-lifted` | `0 1px 3px rgb(0 0 0 / 0.08), 0 4px 12px rgb(0 0 0 / 0.06)` | popovers, dialogs, [data-lifted] |
| `--bf-surface-2` | `color-mix(in oklab, var(--bf-surface), var(--bf-primary) 5%)` | subtle raised surface |
| `--bf-surface-3` | `color-mix(in oklab, var(--bf-surface), var(--bf-primary) 10%)` | stronger raised surface |
| `--bf-overlay` | `color-mix(in oklab, var(--bf-surface), transparent 20%)` | light overlay |
| `--bf-overlay-heavy` | `color-mix(in oklab, var(--bf-surface), transparent 5%)` | heavy overlay (backdrop) |
| `--bf-primary-subtle` | `color-mix(in oklab, var(--bf-primary), transparent 88%)` | very light primary tint |
| `--bf-primary-muted` | `color-mix(in oklab, var(--bf-primary), transparent 75%)` | light primary tint (focus rings) |
| `--bf-primary-strong` | `color-mix(in oklab, var(--bf-primary), transparent 12%)` | strong primary tint |
| `--bf-primary-darken` | `color-mix(in oklab, var(--bf-primary), #000000 8%)` | hover darken for primary fills |
| `--bf-danger-darken` | `color-mix(in oklab, var(--bf-danger), #000000 8%)` | hover darken for danger fills |
| `--bf-danger-muted` | `color-mix(in oklab, var(--bf-danger), transparent 75%)` | light danger tint (focus rings) |
| `--bf-success-muted` | `color-mix(in oklab, var(--bf-success), transparent 75%)` | light success tint (focus rings) |
| `--bf-info-muted` | `color-mix(in oklab, var(--bf-info), transparent 75%)` | light info tint (focus rings) |
| `--bf-warning-muted` | `color-mix(in oklab, var(--bf-warning), transparent 75%)` | light warning tint (focus rings) |
| `--bf-border-strong` | `color-mix(in oklab, var(--bf-border), var(--bf-text) 20%)` | stronger border on hover |
| `--bf-transition` | `150ms ease` | default motion |
| `--bf-transition-slow` | `250ms ease` | slower motion |
| `--bf-disabled-opacity` | `0.5` | dimming for disabled buttons/inputs |

### Layout

| Token | Default | Purpose |
|---|---|---|
| `--bf-content-width` | `64ch` | max measure for prose |
| `--bf-max-width` | `72rem` | .bf-container width |
| `--bf-control-height` | `2.5rem` | buttons/inputs height |
| `--bf-z-dropdown` | `10` | dropdown / menu panels |
| `--bf-z-sticky` | `20` | .bf-sticky chrome |
| `--bf-z-dialog` | `50` | dialogs (modal dialogs use the top layer) |
| `--bf-z-toast` | `60` | toasts — top of the ladder |

### Layout (v1.6: app shell)

| Token | Default | Purpose |
|---|---|---|
| `--bf-grid-min` | `14rem` | minimum track in [data-grid="auto-fit"/"auto-fill"] |
| `--bf-grid-gap` | `var(--bf-space-4)` | default [data-grid] gap |
| `--bf-sidebar-width` | `16rem` | fixed-ish aside in .bf-sidebar |
| `--bf-sticky-top` | `0` | offset for .bf-sticky |

### Layout (v1.8: content & media)

| Token | Default | Purpose |
|---|---|---|
| `--bf-avatar-size` | `2.5rem` | edge of .bf-avatar (matches control height) |
| `--bf-space-1` | `0.125rem` |  |
| `--bf-space-2` | `0.25rem` |  |
| `--bf-space-3` | `0.5rem` |  |
| `--bf-space-4` | `0.75rem` |  |
| `--bf-space-5` | `1rem` |  |
| `--bf-space-6` | `1.5rem` |  |
| `--bf-space-7` | `2rem` |  |
| `--bf-space-8` | `3rem` |  |
| `--bf-radius` | `0.25rem` |  |
| `--bf-radius-sm` | `0.125rem` |  |
| `--bf-radius-lg` | `0.5rem` |  |
| `--bf-control-height` | `2rem` |  |
| `--bf-primary` | `light-dark(#000000, #ffffff)` |  |
| `--bf-primary-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-surface` | `light-dark(#ffffff, #000000)` |  |
| `--bf-surface-alt` | `light-dark(#f0f0f0, #101010)` |  |
| `--bf-text` | `light-dark(#000000, #ffffff)` |  |
| `--bf-muted` | `light-dark(#000000, #ffffff)` |  |
| `--bf-border` | `light-dark(#000000, #ffffff)` |  |
| `--bf-focus-ring` | `light-dark(#000000, #ffffff)` |  |
| `--bf-danger` | `light-dark(#000000, #ffffff)` |  |
| `--bf-danger-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-success` | `light-dark(#000000, #ffffff)` |  |
| `--bf-success-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-info` | `light-dark(#000000, #ffffff)` |  |
| `--bf-info-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-warning` | `light-dark(#000000, #ffffff)` |  |
| `--bf-warning-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-primary` | `light-dark(#000000, #ffffff)` |  |
| `--bf-primary-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-surface` | `light-dark(#ffffff, #000000)` |  |
| `--bf-surface-alt` | `light-dark(#f0f0f0, #101010)` |  |
| `--bf-text` | `light-dark(#000000, #ffffff)` |  |
| `--bf-muted` | `light-dark(#000000, #ffffff)` |  |
| `--bf-border` | `light-dark(#000000, #ffffff)` |  |
| `--bf-focus-ring` | `light-dark(#000000, #ffffff)` |  |
| `--bf-danger` | `light-dark(#000000, #ffffff)` |  |
| `--bf-danger-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-success` | `light-dark(#000000, #ffffff)` |  |
| `--bf-success-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-info` | `light-dark(#000000, #ffffff)` |  |
| `--bf-info-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-warning` | `light-dark(#000000, #ffffff)` |  |
| `--bf-warning-fg` | `light-dark(#ffffff, #000000)` |  |
| `--bf-shadow` | `none` |  |
| `--bf-shadow-lifted` | `none` |  |

<!-- TOKENS:END -->

## How light/dark works (the trick)

Color tokens are `light-dark(lightValue, darkValue)`:

```css
:root {
  color-scheme: light dark;
  --bf-surface: light-dark(#ffffff, #161616);
}
```

The browser resolves `light-dark()` from `color-scheme`, which already
follows the OS. **Dark mode works with zero attributes and zero duplicate
palettes.** The `[data-bf-theme]` attribute simply overrides `color-scheme`:

| Attribute | Effect |
|---|---|
| (none) / `data-bf-theme="auto"` | follow OS preference |
| `data-bf-theme="light"` | force light |
| `data-bf-theme="dark"` | force dark |
| `data-bf-theme="contrast"` | force high-contrast colors |
| `data-bf-theme="editorial"` etc. | starter themes (see below) |

## Making your own theme

New to theming? Follow **[theming-tutorial.md](theming-tutorial.md)** —
a step-by-step "build your first theme" walkthrough.

In short: copy `src/themes/custom.css`, rename it, override only what
you want:

```css
@layer tokens {
  [data-bf-theme="my-theme"] {
    color-scheme: light dark;
    --bf-primary: light-dark(#2563eb, #93b4fd);
    --bf-radius: 0.5rem;
    --bf-font: "Inter", system-ui, sans-serif;
  }
}
```

Then `<html data-bf-theme="my-theme">`. Unset variables inherit from the base
tokens — you never "reset" anything.

## Starter themes

Six built-in demos of how far a few variables go. See the files in
`src/themes/`:

- **Editorial** — serif, paper tones, square corners.
- **Dashboard** — denser controls, blue accent, tighter radii.
- **Playful** — rounded, saturated, bouncier transitions.
- **Forest** — deep greens on warm paper.
- **Sunset** — warm coral daylight, amber dusk, rounder corners.
- **Custom** — a copy-and-rename template for your own theme.

They are the marketing proof: *a handful of variables, completely
different product.*

## OS accessibility settings

Tokens also respect OS settings via media queries at the end of
`src/tokens.css`:

- **`prefers-contrast: more`** — forces black-on-white tokens (pure
  `#000`/`#fff`, no `light-dark()` pairs), so high-contrast users get a
  maximally distinct palette even in a themed app.
- **`prefers-reduced-transparency: reduce`** — drops the lifted shadow to
  `none`, removing depth effects.

An **explicit `data-bf-theme` still wins** over both: a theme selector like
`[data-bf-theme="forest"]` outranks the plain `:root` the media queries set,
so choosing a theme (explicit intent) beats OS preference. The manual
`[data-bf-theme="contrast"]` preset sets the same values as the
`prefers-contrast` block, so the two never fight.

## Typed properties

Tokens ship as **plain custom properties** — Barefoot registers nothing
with `@property`. Nothing in the framework animates a token (they only
ever serve as transition *durations*), and registration would force ten
`initial-value` literals to mirror the palette by hand.

If *your* UI animates a token — say a smooth accent morph on theme
change — register your own copy in userland CSS; the last registration
wins:

```css
@property --bf-primary {
  syntax: "<color>";
  inherits: true;
  initial-value: #yourcolor;
}
html { transition: --bf-primary 300ms; }
```

## Using your own accent in one line

```css
:root { --bf-primary: #yourcolor; }
```

Everything that is "brand" — buttons, links, focus, selection, form
accents — follows, in both light and dark.

## Density axis (v3.4)

Themes gain a second axis — **density** — without new palettes. Set
`data-density="compact"` on `<html>` (or any subtree) to remap the
spacing scale, radii, and control height to tighter values:

```html
<html data-bf-theme="dashboard" data-density="compact">
```

The compact preset overrides:

| Token | Normal | Compact |
|---|---|---|
| `--bf-space-1` | `0.25rem` | `0.125rem` |
| `--bf-space-2` | `0.5rem` | `0.25rem` |
| `--bf-space-3` | `0.75rem` | `0.5rem` |
| `--bf-space-4` | `1rem` | `0.75rem` |
| `--bf-space-5` | `1.5rem` | `1rem` |
| `--bf-space-6` | `2rem` | `1.5rem` |
| `--bf-space-7` | `3rem` | `2rem` |
| `--bf-space-8` | `4rem` | `3rem` |
| `--bf-radius` | `0.375rem` | `0.25rem` |
| `--bf-radius-sm` | `0.25rem` | `0.125rem` |
| `--bf-radius-lg` | `0.625rem` | `0.5rem` |
| `--bf-control-height` | `2.5rem` | `2rem` |

Combine with any theme — the density axis is orthogonal to the color
axis. A dashboard theme with compact density gives denser controls;
an editorial theme without it stays airy. Override individual tokens
to fine-tune the compact preset.
