Theme it, own it.
Wensity is built on plain CSS variables exposed through Tailwind v4's @theme inline. No theme provider, no JS context, no runtime config. Change one file, every component re-skins.
A core promise of Wensity: there is no vendor lock-in. The CLI installs real .tsx files into your repo, and those files reference CSS variables you define. The brand ramp ships as Wensity Red under the token name chili purely as a sensible default. You decide the values, and if you don't like the name you can rename the tokens entirely. We cover both paths below.
Design tokens
Drop this into your globals.css (or any file imported once at the root). It defines the light/dark surface tokens every Wensity component reads.
@import "tailwindcss";/* Dark mode via .dark class on <html> */@custom-variant dark (&:where(.dark, .dark *));@theme inline {/* Surfaces, your values */--color-background: var(--background);--color-foreground: var(--foreground);--color-surface: var(--surface);--color-surface-muted: var(--surface-muted);--color-border: var(--border);--color-border-strong: var(--border-strong);--color-muted-foreground: var(--muted-foreground);/* Brand ramp, the components reference these three steps. */--color-chili-300: #ff8a73;--color-chili-500: #cd1c18;--color-chili-800: #6a0d0e;}:root {--background: #fafafa;--foreground: #0a0a0a;--surface: #ffffff;--surface-muted: #f4f4f5;--border: rgba(10, 10, 10, 0.08);--border-strong: rgba(10, 10, 10, 0.16);--muted-foreground: #52525b;--pattern-fg: rgba(10, 10, 10, 0.07);}.dark {--background: #0a0a0b;--foreground: #f5f5f6;--surface: #111113;--surface-muted: #18181b;--border: rgba(255, 255, 255, 0.08);--border-strong: rgba(255, 255, 255, 0.14);--muted-foreground: #a1a1aa;--pattern-fg: rgba(255, 255, 255, 0.06);}
A note on the structure: the variables under @theme inline are the names Tailwind generates utility classes from (bg-surface, text-foreground, etc). The variables under :root and .dark are where the actual values live. Keep that separation. It lets you swap a whole palette without restating every mapping.
Primitive tokens
UI primitives consume an additional --primitive-* namespace. Shared site tokens (--background, --foreground, chili ramp) still apply; the primitive block adds solid surfaces, control fills, status intents, radius roles, fonts, and overlay shadows used only by /primitives installs.
When you install a primitive, the CLI inserts one delimited block into the CSS file from wensity.json (idempotent, it updates in place). shadcn installs receive the same values through cssVars / css on the registry item. You do not need a React theme provider.
| Token | Purpose |
|---|---|
--primitive-surface-elevated | Dialog / sheet / drawer panels |
--primitive-surface-overlay | Popovers, tooltips, menus, select |
--primitive-control-solid (+ hover/active/foreground) | Primary button / solid control fill |
--primitive-control-height-sm/md/lg | Shared control heights (2rem / 2.25rem / 2.5rem) |
--primitive-radius | Base radius; control/surface/item roles derive from it |
--primitive-success / --warning / --info / --destructive* | Status and destructive intents |
--primitive-font-sans / --display / --mono | Type roles on primitives |
--primitive-shadow-modal / --overlay / --raised | Overlay depth |
--primitive-backdrop | Modal scrim |
Override any of these in your global CSS to retheme every installed primitive. Premium /components do not read --primitive-*, so changing the block will not restyle marketing components.
To generate a shareable style code instead of editing tokens by hand, use the Create preset studio. See also the Primitives install guide.
What each preset field controls
A preset is seven choices. Each one writes a specific set of CSS variables, and nothing else. In particular, base color does not switch light and dark mode — it picks the neutral palette, and both modes are generated for every base color. Toggle the .dark class to change mode.
| Preset field | Writes | Does not affect |
|---|---|---|
| Base color | --background, --foreground, --surface, --surface-muted, --muted, --muted-foreground, --border, --border-strong, plus the neutral --primitive-surface-* / --primitive-text-* roles | Light vs dark mode, accent color |
| Theme color | --primitive-control-solid (+ hover/active/foreground), --primitive-ring, and chart series when Chart palette is Theme | Neutrals and page background |
| Chart palette | --primitive-chart-1 … --primitive-chart-6 | Everything else |
| Heading font | --primitive-font-display and the matching @fontsource import | Body copy |
| Body font | --primitive-font-sans and the matching @fontsource import | Headings |
| Icon library | Import rewriting in installed primitive source (Tabler ↔ Lucide) | CSS variables |
| Radius | --primitive-radius and the control/surface/item roles derived from it | Colors |
wensity init --preset writes two delimited blocks into the CSS file named in wensity.json: Wensity Base Tokens (the semantic layer above) and Wensity Primitive Tokens (the --primitive-* layer). Both are replaced in place on re-run, so edits outside the markers are preserved.
Dark mode
We use the class strategy. Toggle a .dark class on <html>. Pair with next-themes for system / persisted preference.
Wensity is dark-first by design. The dark palette is what we tune against, and most components look slightly more polished in dark mode by default. If your product is light-only, you can ignore the .dark block entirely; nothing will break.
One small but important detail: add suppressHydrationWarning to your <html> tag in the root layout. next-themes (and a few browser extensions) inject attributes before React hydrates, and Next will surface a benign mismatch without it.
Your brand color
Every accent in the library reads from a three-step ramp (300, 500, 800). Override the values and every component re-skins. You're not replacing "our brand" with yours. The slot was always yours. We just shipped a default in it.
/* Override the three brand steps with your values,every component picks them up automatically. */@theme inline {--color-chili-300: #93c5fd; /* tint */--color-chili-500: #2563eb; /* brand */--color-chili-800: #1e3a8a; /* deep */}
Three steps is intentional. The 500 is your on-screen brand color, the 300 is the tint we lean on for highlights and gradient stops, the 800 is the deep tone behind shadows. If you want a fuller scale (50 → 950), declare the entire ramp under @theme inline.
Rename the palette
Don't want the word chili in your stylesheet? You own every component file. Run one project-wide rename and your palette is whatever you want: brand, primary, accent, anything.
# In your editor: project-wide find/replace# chili-300 → brand-300# chili-500 → brand-500# chili-800 → brand-800# Or with ripgrep + sed:rg -l 'chili-(300|500|800)' src \| xargs sed -i '' -E 's/chili-(300|500|800)/brand-\1/g'
Then update your @theme inline block so the new names are defined:
/* Prefer brand-* instead of chili-*? You own the source. */@theme inline {--color-brand-300: #93c5fd;--color-brand-500: #2563eb;--color-brand-800: #1e3a8a;}
That's it. There's no upstream package shouting at you for renaming a token.
Display font
Wensity ships designed against Cabinet Grotesk, but any tight-tracking display face works. Swap the family on --font-display.
@font-face {font-family: "Cabinet Grotesk";src: url("/fonts/CabinetGrotesk-Extrabold.woff2") format("woff2");font-weight: 800;font-display: swap;}@theme inline {--font-display: "Cabinet Grotesk", ui-sans-serif, system-ui, sans-serif;}
The library uses Cabinet for every type role: headlines, body, UI, labels, snippets, and code-like surfaces. We deliberately avoid a second body family or a separate mono role. Most products read better when the rhythm does the work instead of another font.
Pattern + grain
The hatch / grid backgrounds you see in the studio site read from --pattern-fg. Tweak its alpha to dial intensity globally.
The grid texture is always rendered as a CSS background. Never an SVG, never an image. That keeps it crisp at every zoom level and removes a network round-trip from the critical path. We pair it with a fixed-position grain overlay (see components/site/Grain.tsx) for the subtle film noise on dark backgrounds.
Best practices
A short list of theming habits that have saved us pain on dozens of studio engagements.
- Restrict yourself to one accent color. Wensity components are tuned for a single brand hue plus muted grays. The moment you add a second accent, half of them stop reading premium.
- Keep border alphas low (8 to 12% in dark, 6 to 10% in light). Push them above 20% alpha and surfaces start feeling skeuomorphic instead of editorial.
- Start with a flat outer shadow plus a 1px border for general-purpose cards. Inset highlights and shadows are welcome when they are an intentional part of a component or block's visual treatment.
- Don't override component-level colors with
classNameif a token would do the job. Tokens compose, classes fight each other.