Skip to content

Migrate · 2 of 3

Move your tokens to Hoverlab’s

Blocks style themselves through a fixed set of semantic colour names. Getting them to look like your product is a matter of making those names resolve to your values — and there are two colour formats in play, which is where this goes wrong. This page is precise about which export file is a Tailwind v4 @theme block and which is not.

What blocks read

A block never writes a colour. It writes bg-card, text-muted-foreground, border-border. For those to mean anything your project needs two things: a CSS variable for each name, and a way for Tailwind to turn the name into a utility. The names are:

--background --foreground --card --card-foreground --popover --popover-foreground --primary --primary-foreground --secondary --secondary-foreground --muted --muted-foreground --accent --accent-foreground --destructive --destructive-foreground --border --input --ring

Plus --radius, which the corner utilities derive from, and dark mode as a .dark class on an ancestor — the token files switch values on it.

Two colour formats — do not mix them

HSL channelsFinished colours
Looks like--primary: 243 75% 59%;--primary: oklch(0.52 0.19 250);
Used ashsl(var(--primary)) in the Tailwind mappingvar(--primary) in the Tailwind mapping
Emitted byThe scaffolded templates’ globals.css, and the token export’s tokens.cssThe registry’s @hoverlab/hoverlab and preset items, and the free theme generator

The mapping and the variables have to agree. hsl(var(--primary)) around an oklch(…) value is not a colour, so the declaration silently does not apply — no error, the button is just the wrong colour or none. The reverse fails the same way. When a block looks unstyled or oddly transparent after a token change, this is the first thing to check.

Map your names onto ours

These pairings are suggestions — your names will differ. The right-hand column is what blocks actually read.

If yours is…Hoverlab readsUsed for
page backgroundbackgroundThe colour behind everything.
body textforegroundDefault text on the page background.
surface, panel, cardcardRaised areas. Pair with card-foreground for the text on them.
brand, accent, actionprimaryButtons and links. Pair with primary-foreground for text on top.
text on the brand colourprimary-foregroundThe label inside a primary button.
subdued fill, chip, code wellmutedQuiet backgrounds. muted-foreground is the secondary-text colour.
secondary textmuted-foregroundCaptions, helper text, placeholder text.
divider, hairlineborderRules and card outlines.
form-field outlineinputUsually the same value as border.
danger, errordestructiveDelete buttons and error text.
focus outlineringThe focus ring. Usually the brand colour.

The least invasive route keeps your variables where they are and adds Hoverlab’s names beside them, defined in terms of yours. No value is copied, so there is still one place to change a colour.

globals.css
:root {
  /* Left: the names Hoverlab blocks read. Right: yours. */
  --background: var(--surface-0);
  --foreground: var(--text-1);
  --card: var(--surface-1);
  --card-foreground: var(--text-1);
  --primary: var(--brand-600);
  --primary-foreground: var(--on-brand);
  --muted: var(--surface-2);
  --muted-foreground: var(--text-2);
  --border: var(--line);
  --input: var(--line);
  --ring: var(--brand-600);
  /* …and the rest of the list above, until none is missing. */
}

Aliasing keeps whatever format your variables are in, so the Tailwind mapping below has to match it: channels take hsl(var(--x)), finished colours take var(--x).

Which export file is what

The design-system export produces these. The palette on /design-system is live for anyone; downloading the files is a Pro feature.

FileWhat it isA Tailwind v4 @theme block?
tokens.cssPlain CSS variables on :root and .dark, as HSL channels.Not for colours. It carries an @theme block only for the shape: if you moved the spacing or type-scale sliders it adds --spacing and the --text-* ramp there, which v4 reads and v3 ignores.
tailwind-theme.cssMaps each token to a utility, as --color-{name}: hsl(var(--{name})), plus a radius scale and a class-based dark variant.Yes — an @theme inline block. Tailwind v4 only.
tailwind-theme.v3.tsA theme.extend object for tailwind.config.No. A JavaScript config. Tailwind v3 only.
tokens.light.json / tokens.dark.jsonW3C design tokens: colour, radius, spacing and type, one file per mode.No. For design tools and token pipelines.
style-dictionary.config.mjsBuilds the two design-token files into CSS and JS with Style Dictionary.No. For teams feeding other platforms.
figma-variables.json / push-figma-variables.mjsOne Figma collection with Light and Dark modes, and the script that sends it.No. Figma Enterprise plan only.
hoverlab.config.jsonThe brand as four numbers, for the CLI.No.

You need tokens.css plus one of the two Tailwind files. The pair does nothing apart: the first declares --primary, the second is what makes bg-primary a class.

Wiring it into Tailwind v4

app/globals.css
@import "tailwindcss";
@import "./tokens.css";
@import "./tailwind-theme.css";

All three are @import rules, and CSS ignores an import that comes after any other rule — so they go at the very top. If your project already declares a dark variant, delete the @custom-variant dark line from tailwind-theme.css.

If your variables are finished colours instead — from the free generator, or your own — the mapping is the one without hsl(). The generator writes it for you; by hand it is one line per token:

globals.css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  /* one line per token, through ring */
}

Wiring it into Tailwind v3

Merge the exported extend keys into your config rather than replacing it, and keep darkMode on class so the .dark block applies.

tailwind.config.ts
import type { Config } from 'tailwindcss'
import brand from './tailwind-theme.v3'

const config: Config = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: { ...brand.extend.colors /* , your own */ },
      borderRadius: { ...brand.extend.borderRadius },
    },
  },
}

export default config

And put the :root / .dark blocks from tokens.css in your stylesheet. If it has an @theme block at the bottom, delete it: v3 has no such rule.

For your designers

tokens.light.json and tokens.dark.json are W3C design-token documents with hex values, one per mode, because DTCG has no settled syntax for modes and tools disagree about it. Import each into a variables plugin that reads DTCG, the dark file as a second mode on the same collection. The export’s own README names the importers it was written against.

On a Figma Enterprise plan, figma-variables.json skips the two-import step: it is the body of Figma’s Variables API, with Light and Dark already modes of one collection, and push-figma-variables.mjs sends it. Figma only allows that call on Enterprise, so the DTCG files stay the route for every other plan.

What the CLI does with a brand

Put hoverlab.config.json in your project root and npx hoverlab add tints effects to match it. Blocks and pages ignore the file entirely: they follow your tokens, which is the whole point of this page. The effect tinting is a hue rotation and a saturation shift, an approximation that moves every colour in the effect, including deliberately neutral ones. An explicit hue or saturation flag on the command overrides it.

A separate job, for AI tools rather than your build: npx hoverlab dna --brand indigo --out DESIGN.md writes the design system as one document to paste into an agent. --brand takes a preset id (emerald, indigo, rose, amber, cyan, violet, lime, orange, magenta, sky, crimson, teal), not an arbitrary colour, and the command changes nothing in your project. --out writes a file instead of printing.

If it does not look right

You seeUsually
Blocks render with no colour at allThe utilities do not exist: the Tailwind mapping is missing or not imported. bg-card is not a class until it is.
Some colours right, some black or transparentMixed formats: a channels variable behind var(), or a finished colour inside hsl().
Dark mode never switchesNothing puts .dark on an ancestor, or v3 has darkMode unset, or v4 has no dark variant.
Spacing or type looks off after a v4 importThe @theme shape block in tokens.css changed --spacing or the type ramp for your whole project, not just the blocks. Delete the block to undo it.
The free route needs no export at all: the theme generator emits finished colours and the v4 mapping together, and a whole preset installs with npx shadcn add @hoverlab/preset-console.

Something wrong or missing here? Browse the catalog — every page in it links back to the source it documents.