Theming

How the theme is built, what each token is for, and how to change it.

How it works

The theme is one file, styles/base.css, built in two layers:

  • Values. --nui-* variables hold the actual colors, radius, shadows, and sizes. Light values sit in the :root block. The .dark block overrides only the values that change in dark mode.
  • Tokens. An @theme inline block turns each value into a Tailwind token. --nui-card becomes card, which you use as bg-card, border-card, and so on.

Components use tokens only, never raw colors. Change a value and every component that uses it follows, in light and dark.

Tokens are named for their job, not their color. muted-foreground means secondary text, whatever color you give it.

Most grays point at one scale, --nui-neutral-50 to --nui-neutral-950. Shift that scale to make every gray warmer or cooler at once.

Colors

Naming

Color tokens come in families that share a base name:

SuffixMeaning
(none)The fill.
-foregroundText and icons on that fill.
-hover, -activeThe fill on hover and while pressed.
-softA light tint of the color, for quiet backgrounds.
-soft-foregroundText on the tint.

When you change a fill, check that its -foreground still reads well on it.

Surfaces

TokenUsed for
backgroundThe page behind everything.
foregroundDefault text and icons.
canvasThe content area inside an app shell, set apart from the page.
canvas-borderThe canvas edge. Transparent by default.
cardCards, tables, and panels.
card-foregroundText on cards.
card-borderCard edges.
popoverMenus, dropdowns, date pickers, and other floating panels.
popover-foregroundText in floating panels.
mutedQuiet fills, such as skeletons, progress tracks, segmented controls, and code blocks.
muted-foregroundSecondary text: descriptions, hints, and placeholders.
borderThe default hairline for dividers and outlines.
tagTag and keyboard key fills.
tag-borderTag and keyboard key edges.
overlayThe dimmed backdrop behind dialogs and drawers.
inverseHigh-contrast fills that flip against the page, such as tooltips.
inverse-foregroundText on inverse fills.

Controls

TokenUsed for
controlThe fill of inputs, selects, checkboxes, and outline buttons.
control-foregroundText inside controls.
control-borderControl edges.
control-hoverThe control fill on hover.
control-activeThe control fill while pressed.
control-focusThe control edge while focused. Follows ring by default.
control-disabledThe fill of disabled controls.
control-disabled-foregroundText in disabled controls.
inputUnfilled tracks and lines, such as an off switch, step and timeline connectors, and the upload drop area.
ringThe keyboard focus ring. Follows primary by default.

Actions

TokenUsed for
primaryMain buttons, checked and selected states, active tabs, and progress.
primary-foregroundText on primary.
primary-hoverPrimary on hover.
primary-activePrimary while pressed.
primary-softA light primary tint, such as a selected date range.
secondarySecondary buttons.
secondary-foregroundText on secondary buttons.
secondary-hoverSecondary on hover.
secondary-activeSecondary while pressed.
accentHovered and highlighted items in menus, lists, and calendars.
accent-foregroundText on accent.
accent-hoverAccent on hover.

Status

TokenUsed for
destructiveDelete actions, errors, and invalid fields.
destructive-foregroundText on destructive.
destructive-softA light destructive tint, such as an error alert background.
destructive-soft-foregroundText on the destructive tint.
successCompleted and healthy states.
success-foregroundText on success.
success-softA light success tint, such as a success alert background.
success-soft-foregroundText on the success tint.
warningSomething that needs attention.
warning-foregroundText on warning.
warning-softA light warning tint, such as a warning alert background.
warning-soft-foregroundText on the warning tint.
infoNeutral notices and tips.
info-foregroundText on info.
info-softA light info tint, such as an info alert background.
info-soft-foregroundText on the info tint.

A destructive button uses the solid fill with its foreground; an alert uses the soft tint with its soft foreground.

Palette

Ten more colors for content that isn't a status. The full calendar uses them for events, and blocks use them for tags, categories, and avatars.

TokenUsed for
palette-emeraldThe emerald fill.
palette-emerald-foregroundText on the emerald fill.
palette-emerald-softA light emerald tint.
palette-emerald-soft-foregroundText on the emerald tint.
palette-roseThe rose fill.
palette-rose-foregroundText on the rose fill.
palette-rose-softA light rose tint.
palette-rose-soft-foregroundText on the rose tint.
palette-blueThe blue fill.
palette-blue-foregroundText on the blue fill.
palette-blue-softA light blue tint.
palette-blue-soft-foregroundText on the blue tint.
palette-cyanThe cyan fill.
palette-cyan-foregroundText on the cyan fill.
palette-cyan-softA light cyan tint.
palette-cyan-soft-foregroundText on the cyan tint.
palette-orangeThe orange fill.
palette-orange-foregroundText on the orange fill.
palette-orange-softA light orange tint.
palette-orange-soft-foregroundText on the orange tint.
palette-redThe red fill.
palette-red-foregroundText on the red fill.
palette-red-softA light red tint.
palette-red-soft-foregroundText on the red tint.
palette-purpleThe purple fill.
palette-purple-foregroundText on the purple fill.
palette-purple-softA light purple tint.
palette-purple-soft-foregroundText on the purple tint.
palette-yellowThe yellow fill.
palette-yellow-foregroundText on the yellow fill.
palette-yellow-softA light yellow tint.
palette-yellow-soft-foregroundText on the yellow tint.
palette-limeThe lime fill.
palette-lime-foregroundText on the lime fill.
palette-lime-softA light lime tint.
palette-lime-soft-foregroundText on the lime tint.
palette-grayThe gray fill.
palette-gray-foregroundText on the gray fill.
palette-gray-softA light gray tint.
palette-gray-soft-foregroundText on the gray tint.

The solid fill's variable ends in -solid, for example --nui-palette-blue-solid.

Charts

Chart series take these colors in order. By default they reuse primary and the palette, so charts match the rest of the theme.

TokenUsed for
chart-1Series 1. Defaults to primary.
chart-2Series 2. Defaults to palette-purple.
chart-3Series 3. Defaults to palette-emerald.
chart-4Series 4. Defaults to palette-yellow.
chart-5Series 5. Defaults to palette-red.
chart-6Series 6. Defaults to palette-orange.
chart-7Series 7. Defaults to palette-cyan.
chart-8Series 8. Defaults to palette-rose.
chart-9Series 9. Defaults to palette-lime.
chart-10Series 10. Defaults to palette-gray.

Shape and type

Radius

TokenUsed for
rounded-controlButtons, inputs, and selects.
rounded-control-smItems inside controls and menus, segments, and tooltips.
rounded-cardCards.
rounded-popoverMenus, dropdowns, and date pickers.
rounded-overlayDialogs and drawers.
rounded-tagTags and keyboard keys.
rounded-badgeBadges and the switch track. Fully round by default.
rounded-canvasThe canvas inside an app shell.

These point at a base scale, --nui-radius-xs to --nui-radius-2xl.

Shadows

TokenUsed for
shadow-cardCards.
shadow-controlControls at rest. None by default.
shadow-popoverMenus and other floating panels.
shadow-dialogDialogs.
shadow-drawerDrawers.
shadow-canvasThe canvas inside an app shell.

Dark mode has its own shadows, which add a faint light edge so surfaces stay visible on dark backgrounds.

Sizes

TokenUsed for
control-smSmall control height, 32 px.
control-mdMedium control height, 36 px.
control-lgLarge control height, 48 px.

Use them with h-, min-h-, or w-, for example h-control-md.

Stacking

TokenUsed for
z-stickySticky headers.
z-dropdown, z-popover, z-overlay, z-modalFloating panels, dialogs, and drawers.
z-toastToasts.
z-tooltipTooltips, above everything else.

If your app's own layers need to sit above or below these, change the matching --nui-z-* values.

Font

font-sans is the interface font: Inter, falling back to system fonts. font-mono is used for code. To use another font, change --font-sans.

Dark mode

Use an explicit dark or light class on <html>. If your appearance controller updates the root directly, keep both classes and the attribute aligned with the same mode value passed to your component provider:

const mode = isDark ? "dark" : "light";
const root = document.documentElement;
root.classList.toggle("dark", mode === "dark");
root.classList.toggle("light", mode === "light");
root.dataset.theme = mode;

The .dark class activates Tailwind dark: utilities as well as the dark tokens. A data-theme attribute alone selects tokens and is not a complete appearance switch. With the exported Theme Workbench wrapper, change its mode prop instead; it synchronizes the root signals as described below.

The .dark block lists only what changes. Surfaces, borders, controls, and the soft tints get dark values; primary and the solid status colors stay the same in both modes.

Change a value

Edit the variables in styles/base.css. For example, to change the primary color:

styles/base.css
:root,
.light,
[data-theme='light'] {
    --nui-primary: #0f766e;
    --nui-primary-foreground: #ffffff;
    --nui-primary-hover: #14897f;
    --nui-primary-active: #0b5c55;
}

--nui-primary isn't in the .dark block, so this changes both modes. To give dark mode its own value, add the same variable to the .dark block.

Change a fill and its -foreground, -hover, and -active together, so text stays readable and states stay distinct.

Theme workbench

The theme workbench lets you build a theme visually instead of editing values by hand.

  1. Pick a starting style: Default, Soft Flat, Neomorphism, Neobrutalism, Glass, or Editorial.
  2. Adjust colors, font, density, and control sizes. Check them against real components in light and dark.
  3. Open Code to preview or download nateui-theme.zip. It contains base.css, config-provider.tsx and DESIGN.md from the same effective settings and selected Light/Dark mode.
  4. Replace your styles/base.css with the downloaded base.css.
  5. Save config-provider.tsx in your project and wrap your app with it:
import { ThemeConfigProvider } from "@/components/theme-config-provider";
 
<ThemeConfigProvider>
  <App />
</ThemeConfigProvider>

The file carries the defaults and extra class names your chosen style depends on, so styles such as Neobrutalism look right beyond their colors.

Use the exported provider once at the application root. Its default mode is the selected appearance; on client mount and mode changes it synchronizes the root .light/.dark class and data-theme attribute before paint. For server-rendered pages, initialize the <html> class to that same mode before first paint, then hydrate with the matching provider mode. Pass a new mode prop to switch appearance at runtime.

Both palettes remain in base.css. With no root signal, CSS supplies the selected mode's token fallback; complete appearance also requires the provider's root-class initialization or matching server markup. The .dark class activates Tailwind dark: utilities. Setting data-theme alone selects tokens and is not a complete appearance switch. Keep an existing appearance controller as the mode source when adopting this wrapper, and avoid competing or nested root-mode owners. Font files are not included; retain your application's font loading and adjust import aliases to your project.

DESIGN.md records the selected preset and guide versions, effective configuration and composition guidance. It replaces affected baseline claims when token, font, default or slot customizations change their assumptions. Exact values stay in the paired CSS and provider files. This selected direction differs from the provisional Default document seeded by skill installation.

Review and deliberately merge or adopt the exported document. Preserve an existing project-owned DESIGN.md until you choose to revise it, and record your actual implementation paths after adoption. Keep its relative links beside the paired files or update them when moving the document. Downloading a guide applies no styles and does not replace project files.

Component defaults

ConfigProvider sets defaults for every component below it, so you don't repeat the same props across your app.

components/providers.tsx
"use client";
 
import type { ReactNode } from "react";
import { ConfigProvider, defaultConfig } from "@/components/ui/config-provider";
 
export function Providers({ children }: { children: ReactNode }) {
  return (
    <ConfigProvider
      value={{
        ...defaultConfig,
        componentDefaults: {
          button: { size: "sm", variant: "solid" },
          input: { size: "sm" },
        },
        classNames: {
          button: { root: "tracking-tight" },
        },
      }}
    >
      {children}
    </ConfigProvider>
  );
}
  • componentDefaults sets default props per component.
  • classNames adds classes to named parts of a component everywhere it appears.

A prop set directly on a component always wins over the default.