ns-ui / theming
Ten tokens, two themes.
Every component reads color from CSS custom properties already in scope, never a hardcoded hex — in markup or in canvas/SVG draw code. This is the full list, what each one controls, and how to change what they resolve to.
The tokens
Values as this site defines them (app/globals.css). A consuming project can set these to anything — the names and what they mean are the contract, not the hex values below.
| Token | Light | Dark | Controls |
|---|---|---|---|
| --background | #ffffff | #0a0a0a | Page background. |
| --foreground | #171717 | #ededed | Body text. |
| --surface | #fafafa | #171717 | Elevated surfaces — cards, code blocks, inset panels. |
| --border | #ebebeb | #2e2e2e | Hairlines and dividers. |
| --ns-muted | #4d4d4d | #8f8f8f | Secondary text — descriptions, captions, eyebrows. |
| --ns-accent | #006bff | #006bff | The brand blue. Links, active states, primary actions. |
| --ns-accent-hover | #0059d1 | #0059d1 | Hover/pressed state for anything using --ns-accent. |
| --error | #ea001d | #ff6369 | Destructive actions, invalid state, failure text. |
| --success | #2d7a2d | #47a447 | Positive/confirmation state. |
| --warning | #7a5200 | #f5a623 | Caution state — never a brand accent. |
--ns-accent and --ns-accent-hover are the same value in both themes — the brand blue is deliberately theme-invariant, everything else here is not.
Two layers, not one
A component's markup never writes var(--ns-muted) directly — it uses a Tailwind utility, text-ns-muted. That utility only exists because it's registered in a second block, separate from the custom property itself:
Layer 1 — the raw custom properties
:root {
color-scheme: light;
--background: #ffffff;
--foreground: #171717;
--surface: #fafafa;
--border: #ebebeb;
--ns-muted: #4d4d4d;
--ns-accent: #006bff;
--ns-accent-hover: #0059d1;
--error: #ea001d;
--success: #2d7a2d;
--warning: #7a5200;
}
.dark {
color-scheme: dark;
--background: #0a0a0a;
--foreground: #ededed;
--surface: #171717;
--border: #2e2e2e;
--ns-muted: #8f8f8f;
--error: #ff6369;
--success: #47a447;
--warning: #f5a623;
}Layer 2 — the Tailwind utility mapping
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-surface: var(--surface);
--color-border: var(--border);
--color-ns-muted: var(--ns-muted);
--color-ns-accent: var(--ns-accent);
--color-ns-accent-hover: var(--ns-accent-hover);
}Tailwind v4's @theme inline generates text-*/bg-*/border-* utilities from --color-* variables, resolved at build time. Copy only the :root/.dark block into a project and the custom properties exist, but text-ns-muteddoesn't — Tailwind never sees a reason to generate it. The component renders with no error and no color: unstyled ink. Both blocks have to land together.
Light and dark
Dark mode is a .dark class on <html>, not a media query — every token above is redeclared inside .dark and the cascade does the rest. color-scheme is set alongside it (light in :root, dark in .dark) so native browser chrome — the scrollbar, a native <select> panel, autofill backgrounds — follows the same theme instead of staying light against a dark page.
Every component in the registry is verified in both themes, and the gate hard-fails a component whose light and dark render as byte-identical screenshots — that only catches a component that ignored theming entirely, not one that merely looks wrong in light. Whatever theme you ship, look at both yourself before calling it done.
Overriding a value
Change what a token resolves to by redeclaring it in your own :root/.dark — components read the variable, not a specific value, so a different --ns-accent re-themes every installed component that uses it, with no component code to touch. This is also what running shadcn add does automatically the first time you install a component that needs a token you don't have yet — it merges that token into your CSS file rather than failing or silently doing nothing.
Why --ns-* and not --muted / --accent
Stock shadcn already ships tokens named --muted and --accent, but as neutral surface colors — a muted background and a subtle hover background, each with its own -foreground pair for text on top of them. This registry needed different things under similar-sounding names: --ns-muted is a text color (secondary body text, captions), and --ns-accent is the brand blue used as a foreground/ink color on links and active states, not a background.
Reusing the stock names would mean a component's text-accent resolves to whatever neutral hover-surface color a project's own shadcn setup already defined for --accent — silently, with no error, because the property exists and just holds the wrong kind of color for the job. Namespacing under --ns-* keeps the two vocabularies from ever aliasing each other: a project can define both shadcn's own tokens and ns-ui's side by side with no collision.