# Theming

> Getting the stylesheet onto the page, and changing every colour, radius and font from one call.

Every component reads the same custom properties, so a theme is a set of overrides on `:root` — no build step, no configuration file, and no component that has to be told about it.

## Getting the styles in

`styles()` returns a `<link>` to one file, which the browser caches across every page of the site. There is nothing to configure and nothing to copy: sitelo’s plugin serves it in dev and writes it into the build, at the same base as the component runtime.

`src/index.ht.js`

```javascript
import { styles } from 'sitelo/ui'

head(
  title('My site'),
  styles(),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">
```

The name carries a hash of the contents, so you can serve it `immutable` and still ship a change. Pass `{ hash: false }` for a plain `/su/ui.css`, or `base` to point the link at a copy you host yourself.

`{ inline: true }` puts the whole sheet in a `<style>` instead — about 11 kB gzipped in every page, but no extra request and nothing that can go missing from `dist/`. That is the better trade for a single page; the link buys its request back on the second page a visitor reads.

`src/index.ht.js`

```javascript
head(
  title('My site'),
  styles({ inline: true }),
)
```

The rest of the family hands you the pieces. `stylesheet()` returns the raw CSS as a string — for hosting the sheet somewhere sitelo cannot reach, or writing it somewhere yourself — and `stylesUrl()` the href alone, for a link element of your own.

## Presets

A preset changes the whole look at once. `styles({ preset: 'neumorphism' })` links a second sheet straight after the core one — served, hashed and cached on the same terms — and every component on the page follows it, with nothing to change in the markup.

`src/index.ht.js`

```javascript
import { styles } from 'sitelo/ui'

head(
  title('My site'),
  styles({ preset: 'neumorphism' }),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">
// <link rel="stylesheet" href="/su/neumorphism-5d0e7b91.css">
```

Each preset has a page of its own, with every component restyled live: [Neumorphism](https://sitelo.dev/ui/theming/neumorphism), [Neubrutalism](https://sitelo.dev/ui/theming/neubrutalism), [Superneon](https://sitelo.dev/ui/theming/superneon), [Terminal](https://sitelo.dev/ui/theming/terminal), [Blueprint](https://sitelo.dev/ui/theming/blueprint).

`inline` inlines both sheets, `stylesheet({ preset })` returns them as one string, and a name that is not a preset throws with a list of the ones there are.

## Overriding tokens

`theme()` writes the overrides. Keys are camelCase token names, palette objects, or literal custom properties — and it goes `after` `styles()`, so it wins.

`src/index.ht.js`

```javascript
import { styles, theme } from 'sitelo/ui'

head(
  styles(),
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', active: '#3f3fb0', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }),
)
```

## Scoped themes

A `selector` scopes the overrides to a subtree instead of the whole page. That is what the three panels below do — same components, three different palettes, one page.

```javascript
fragment(
  theme({ primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff', soft: '#e6e6fa', softFg: '#33338f', border: '#b9b9ee' } }, { selector: '.theme-indigo' }),
  theme({ primary: { base: '#b0357a', hover: '#962e68', fg: '#ffffff', soft: '#fbe4f0', softFg: '#7d1f53', border: '#f0a9ce' } }, { selector: '.theme-pink' }),
  theme({ radiusMd: '999px', radiusLg: '1.5rem' }, { selector: '.theme-round' }),
  grid({ min: '11rem' },
    div({ class: 'theme-indigo' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'indigo'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-pink' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'pink'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-round' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'round'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
  ),
)
```

## Dark mode

Dark resolves from `prefers-color-scheme` on its own. An explicit `data-theme` or `data-su-theme` of `light` or `dark` on any ancestor overrides it — which is how the demos on this site follow the toggle in the top bar.

Pass `dark` for overrides that should only apply there. It covers the attribute and the media query in one go.

`src/index.ht.js`

```javascript
theme({
  primary: { base: '#5b5bd6' },
}, {
  dark: { primary: { base: '#8f8ff0' } },
})
```

## What there is to override

Five palettes of nine slots each, a spacing scale, type, radii, shadows and the surface colours. Every one is a custom property — open the stylesheet, or your browser’s inspector, and they are all on `:root`.

```javascript
stack({ gap: 'md' },
  stack({ direction: 'row', gap: 'sm', wrap: true },
    ...['primary', 'neutral', 'success', 'warning', 'danger'].map((color) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: 3.5rem; height: 2rem; border-radius: 0.4rem; background: var(--su-' + color + ')' }),
        text({ variant: 'caption', tone: 'muted' }, color),
      ),
    ),
  ),
  stack({ direction: 'row', gap: 'sm', wrap: true, align: 'flex-end' },
    ...['xs', 'sm', 'md', 'lg', 'xl'].map((step) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: var(--su-space-' + step + '); height: 2rem; border-radius: 0.2rem; background: var(--su-neutral)' }),
        text({ variant: 'caption', tone: 'muted' }, step),
      ),
    ),
  ),
)
```

## Naming

A camelCase key becomes a kebab-case property: `radiusMd` is `--su-radius-md`, `fontSans` is `--su-font-sans`. A nested object expands the same way — `{ primary: { softFg: … } }` sets `--su-primary-soft-fg` — and a key already starting with `--` is used exactly as written, which is the escape hatch for anything the mapping does not cover.

A palette has nine slots: `base`, `hover`, `active`, `fg`, `soft`, `softHover`, `softFg`, `border` and `ring`. Set only the ones you are changing.

## Contrast

The shipped palettes clear WCAG AA against the surfaces they sit on, in both themes, and there is a test in the repository that fails the build if that stops being true. A theme of your own is not covered by it — check your `fg` against your `base` before shipping.

## Props

`styles()`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `preset` | `'neumorphism' \| 'neubrutalism' \| 'superneon' \| 'terminal' \| 'blueprint'` | — | Restyle every component with a preset, linked or inlined after the core sheet. |
| `inline` | `boolean` | `false` | Emit the CSS itself rather than a link to it. |
| `hash` | `boolean` | `true` | Content-hash the file name. Linked only. |
| `base` | `string` | `'/su/'` | Point the URL elsewhere; that copy is yours to host. Linked only. |
| `minify` | `boolean` | `true` | Strip comments and whitespace. Inline only. |
| `nonce` | `string` | — | CSP nonce for the emitted element. |

`stylesUrl()` takes `base` and `hash`; `stylesheet()` takes `minify` and `preset`.

`theme(tokens, options)`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `selector` | `string` | `':root'` | Scope the overrides to a subtree. |
| `dark` | `object` | — | Overrides applied only in dark mode. |
| `nonce` | `string` | — | CSP nonce. |
