# Theming

> Theme tweakit panels: the theme option, live setTheme(), every token, raw --twk-* custom properties, and ready-made recipes.

The page: https://ryankiley.github.io/tweakit/theming.html. Every page as Markdown: https://ryankiley.github.io/tweakit/llms.txt

The kit's entire appearance runs on `--twk-*` custom properties; a theme is just a bag of overrides. Every key is optional: a partial theme moves only what it names, and everything else keeps the default monochrome look.

## Light & dark

The kit ships both looks. Dark is the default, and panels follow the OS to light on their own (`prefers-color-scheme`), like this whole site does. To pin a subtree, set `data-twk-scheme="light"` or `"dark"` on any ancestor: forcing beats the OS preference, and portaled popovers carry the scheme with them. Flip the segmented control and the panel themes itself, whatever your system is set to.

Each color token is a `light-dark()` pair picked by the panel's `color-scheme`, which needs Chrome and Edge 123, Firefox 120 or Safari 17.5.

```js
const panel = tweaks("Scheme", {
  scheme: { type: "segmented", options: ["Auto", "Light", "Dark"], value: "Auto" },
  glow: [24, 0, 80, 1],
  tint: "#7C5CFF",
  live: true,
});
mount.append(panel.el);

panel.on((p) => {
  if (p.scheme === "Auto") delete mount.dataset.twkScheme;
  else mount.dataset.twkScheme = p.scheme.toLowerCase();
});
```

`mount` is the panel's slot inside the stage. In your own page you'd just `document.body.append(panel.el)`.

## Theme at construction

Pass `{ theme }` as the third argument, keyed by token, each value a CSS value; `--twk-accent` is the big one (the default look is deliberately accentless).

```js
const panel = tweaks("Accented", {
  warmth: [0.6, 0, 1, 0.05],
  contrast: [1.1, 0.5, 2, 0.05],
  enabled: true,
}, {
  theme: { "--twk-accent": "#7C5CFF", "--twk-radius": "12px" },
});
mount.append(panel.el);
```

## Live retheming

`panel.setTheme(theme)` re-themes a mounted panel on the fly, and `setTheme(null)` reverts to the default. Here one tweakit panel themes another: the editor on the right drives the sample on the left.

```js
const sample = tweaks("Sample", {
  glow: [24, 0, 80, 1],
  tint: "#7C5CFF",
  mode: ["soft", "hard"],
  on: true,
}, { draggable: false });
target.querySelector(".th-slot").append(sample.el);

const editor = tweaks("Theme", {
  accent: "#7C5CFF",
  base: "#242424",
  radius: [8, 0, 24, 1],
  density: { type: "slider", value: 32, min: 24, max: 44, unit: "px" },   // row height
  back: { type: "button", label: "setTheme(null)", action: () => {
    editor.reset();          // restore the editor's controls first…
    sample.setTheme(null);   // …then drop the override entirely
  } },
});
mount.append(editor.el);

editor.on((t) => sample.setTheme({
  "--twk-accent": t.accent, "--twk-base": t.base,
  "--twk-radius": t.radius + "px", "--twk-row-height": t.density + "px",
}));
```

## Every token

The tokens a theme usually moves (all optional, from the `Theme` type):

| Token | Drives |
| --- | --- |
| `--twk-accent` | slider fills, focus rings, active highlights |
| `--twk-on-accent` | text drawn on the accent (active segment, radio label); the primary text tone when unset, so set it with a light accent |
| `--twk-base` | panel background, reused for recessed wells |
| `--twk-dropdown-bg` | popover / dropdown background |
| `--twk-surface`, `-hover`, `-active` | control surfaces and their interaction steps |
| `--twk-border`, `--twk-border-hover` | hairline borders |
| `--twk-selection` | text-selection wash |
| `--twk-text-root`, `-section`, `-primary`, `-label`, `-secondary`, `-tertiary`, `-focus` | the text hierarchy: title, section, values, labels, muted, faint, focus |
| `--twk-success`, `--twk-danger` | copy-confirmation and invalid-input accents |
| `--twk-shadow-dropdown`, `--twk-shadow-panel`, `--twk-shadow-panel-lifted` | popover, panel and floating elevations |
| `--twk-font-sans`, `--twk-font-mono` | font stacks |
| `--twk-radius`, `--twk-row-height` | corner radius and row height |
| `--twk-width` | the panel's width, 256px by default |

## Any token, or plain CSS

Any `--twk-*` key passes straight through as a custom property, the ones the table leaves out too; a key that isn't a token is skipped with a console warning. And because the whole kit renders from these variables, plain page CSS works as well: set them on `.twk-panel`, scoped by any ancestor. A panel built with `shadow: true` takes them from its host instead: set them on `.twk-host`, and its panes take them too.

```js
tweaks("Raw", schema, { theme: {
  "--twk-accent": "#39d353",
  "--twk-ease-out": "ease-in-out",   // any token rides along
} });

/* or, in plain CSS, no JS at all: */
.my-sidebar .twk-panel { --twk-accent: #39d353; --twk-radius: 4px; }
```

## Recipes

Three starting points beyond the default monochrome. Each panel below is live, built with the theme object printed underneath.

```js
const gallery = target.querySelector(".th-gallery");
// Dark-base recipes carry a full light text-tone set, so titles / labels / toolbar
// icons aren't dark-on-dark on a light page; each sets the text on its accent, so the
// active pill's label stays legible. Every tone clears WCAG AA.
const recipes = {
  Daylight: { "--twk-base": "#f2f1ee", "--twk-accent": "#5b4dff", "--twk-on-accent": "#ffffff", "--twk-dropdown-bg": "#ffffff",
              "--twk-surface": "rgba(0, 0, 0, 0.05)", "--twk-surface-hover": "rgba(0, 0, 0, 0.09)",
              "--twk-border": "rgba(0, 0, 0, 0.08)", "--twk-selection": "rgba(0, 0, 0, 0.15)",
              "--twk-text-root": "#1a1a1a", "--twk-text-primary": "rgba(0, 0, 0, 0.85)", "--twk-text-section": "rgba(0, 0, 0, 0.6)",
              "--twk-text-label": "rgba(0, 0, 0, 0.6)", "--twk-text-secondary": "rgba(0, 0, 0, 0.58)", "--twk-text-tertiary": "rgba(0, 0, 0, 0.46)" },
  Terminal: { "--twk-base": "#0d1117", "--twk-accent": "#39d353", "--twk-on-accent": "#161616",
              "--twk-font-sans": "ui-monospace, Menlo, monospace", "--twk-radius": "4px",
              "--twk-surface": "rgba(57, 211, 83, 0.07)", "--twk-border": "rgba(57, 211, 83, 0.18)",
              "--twk-text-root": "#d6ffe0", "--twk-text-primary": "rgba(201, 255, 216, 0.92)", "--twk-text-section": "rgba(201, 255, 216, 0.78)",
              "--twk-text-label": "rgba(201, 255, 216, 0.72)", "--twk-text-secondary": "rgba(201, 255, 216, 0.64)", "--twk-text-tertiary": "rgba(201, 255, 216, 0.5)" },
  Cozy: { "--twk-base": "#241a14", "--twk-accent": "#ff8a5b", "--twk-on-accent": "#161616", "--twk-radius": "16px", "--twk-row-height": "36px",
          "--twk-surface": "rgba(255, 138, 91, 0.08)", "--twk-border": "rgba(255, 138, 91, 0.14)",
          "--twk-text-root": "#fbeada", "--twk-text-primary": "rgba(250, 235, 222, 0.92)", "--twk-text-section": "rgba(250, 235, 222, 0.76)",
          "--twk-text-label": "rgba(250, 235, 222, 0.7)", "--twk-text-secondary": "rgba(250, 235, 222, 0.62)", "--twk-text-tertiary": "rgba(250, 235, 222, 0.5)" },
};
for (const [name, theme] of Object.entries(recipes)) {
  const slot = document.createElement("div");
  slot.className = "th-recipe";
  gallery.append(slot);
  const panel = tweaks(name, {
    level: [60, 0, 100, 1],
    mode: ["auto", "manual"],
    on: true,
  }, { theme, draggable: false });
  slot.append(panel.el);
}
```
