Skip to content

Theming

One layer of --newt-* custom properties drives every component in every flavour. Override a variable rather than a component.

Every colour, radius, shadow, font and duration in newt/ui is a CSS custom property. There is no theme object, no provider and no build step involved in changing one: the components read variables, so the variables are the theme.

The values live in one file, packages/cli/registry/html/tokens.css, and everything else reads from it — the Tailwind theme maps its namespaces onto the same variables, and the plain CSS components use var(--newt-brand) directly.

Token convention

A token name says what the value is for, not what it looks like. --newt-danger rather than --newt-red, --newt-bg-elevated rather than --newt-grey-700. That is what lets a light palette override the same names without any component knowing a palette changed.

Group Tokens
Surfaces --newt-bg-base, --newt-bg-surface, --newt-bg-elevated, --newt-bg-floating, --newt-bg-input, --newt-bg-input-elevated, --newt-bg-hover, --newt-bg-active, --newt-border
Text --newt-text-primary, --newt-text-secondary, --newt-text-muted, --newt-text-link
Brand --newt-brand, --newt-brand-hover, --newt-brand-active
Status --newt-online, --newt-idle, --newt-dnd, --newt-offline, --newt-danger, --newt-danger-hover
Shape --newt-radius-sm, --newt-radius-md, --newt-radius-lg, --newt-radius-full
Type --newt-font-sans, --newt-font-mono, --newt-font-display
Elevation --newt-shadow-low, --newt-shadow-high
Motion --newt-duration-fast, --newt-duration-base, --newt-ease

Every value in the dark palette, with its light counterpart, is listed on the colours page.

Override a token

Put the override after the token block in your own stylesheet. It reaches every component that reads it, in all three flavours, at once.

:root {
  /* one brand hue, and every button, link and focus ring follows */
  --newt-brand: #7a5af8;
  --newt-brand-hover: #6b4ce0;
  --newt-brand-active: #5a3fc0;
}

Scope it to a subtree instead, and only that subtree changes:

.support-panel {
  --newt-brand: var(--newt-online);
}

The light palette

A light surface set ships alongside the dark one and is opt-in. It overrides the surface, text, status and shadow tokens; everything else is shared.

<html data-newt-theme="light">
  <body class="newt-root">
    …
  </body>
</html>

.newt-light on any wrapper does the same thing for a subtree. This site uses the attribute — the toggle in the header sets it, which is why the chrome and the live demos change together.

See Dark mode for wiring a persisted toggle in your own project.

Tailwind v3 and v4

The registry ships the theme three ways so one registry serves both majors, and newtui init writes whichever your project needs.

On Tailwind v3 the tokens arrive as a preset merged into theme.extend:

import { newtPreset } from "./newt-preset"

export default {
  presets: [newtPreset],
  content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
}

On Tailwind v4 there is no JS config. The tokens are an @theme block, and the utilities are generated from the namespaced properties in it:

@import "tailwindcss";

@theme {
  --color-newt-brand: var(--newt-brand);
  --color-newt-bg-base: var(--newt-bg-base);
  /* … */
}

Either way the utilities are the same: bg-newt-brand, text-newt-text-muted, border-newt-border, rounded-md, shadow-elevation-high, duration-fast, ease-newt.

Adding a token

If a value you need has no token, add the token rather than the value. Edit tokens.css first — it is the source — then run node scripts/gen-registry.mjs so the Tailwind v3 and v4 projections and the registry theme item pick it up. A hardcoded hex is the one way to end up with a component that no longer follows the rest.