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.