shadcn Theme Editor
Tune the tokens against real components in both modes, then install the result with one command
Install this theme
The whole theme is in the URL — nothing is stored on our sidenpx shadcn@latest add "https://hoverlab.dev/r/theme.json?t=e30"Moves primary, ring, accent tint, the sidebar and the chart wheel together.
How saturated the accent is. Above about 0.24 the chart colours start colliding for a dichromat.
--radius. The sm / md / lg / xl steps are derived from it.
How far the greys lean toward the brand hue. A little goes a long way.
Individual tokens
All derived from the sliders
The page, and the two things that sit on top of it.
--background#ffffff--foreground#080a0d--card#ffffff--card-foreground#080a0d--popover#ffffff--popover-foreground#080a0d
Primary is the only token most people ever change.
--primary#0068d0--primary-foreground#f7fafe--ring#7da5cf
Secondary, muted and accent are all neutrals at rest.
--secondary#f2f5f9--secondary-foreground#15171a--muted#f2f5f9--muted-foreground#6d7073--accent#f2f5f9--accent-foreground#15171a
Destructive stays red across every hue — a warning that shifts with the brand stops reading as a warning.
--destructive#e7000b--destructive-foreground#fafafa--border#e2e6e9--input#e2e6e9
Spread by hue and separated by lightness, so the series survive colour blindness and greyscale print.
--chart-1#0094ff--chart-2#750085--chart-3#c52400--chart-4#cdc000--chart-5#00ffe5
Its own surface set, so a sidebar can sit a step away from the page.
--sidebar#f7fafe--sidebar-foreground#080a0d--sidebar-primary#0068d0--sidebar-primary-foreground#f7fafe--sidebar-accent#f2f5f9--sidebar-accent-foreground#15171a--sidebar-border#e2e6e9--sidebar-ring#7da5cf
Keep this theme. This browser remembers what you last had open. A free account remembers the ones you name, on every machine you use.
Project settings
Muted text, which is where most themes quietly fail AA.
Popover surface
Sidebar and popover
Project settings
Muted text, which is where most themes quietly fail AA.
Popover surface
Sidebar and popover
Text contrast, both modes
20 pairs clear AAEvery text-on-surface pair in this theme clears 4.5:1 in both light and dark — body text, cards, popovers, all four button variants, the sidebar and the muted helper text that most themes get wrong.
Chart colours under colour blindness
all distinctThe five series stay five colours under protanopia, deuteranopia and tritanopia. The lightnesses are solved onto the hues rather than fixed, which is what makes that hold as you turn the hue.
Same model as the simulator. A legend should still carry a second cue — no palette makes colour alone sufficient.
/* A shadcn/ui theme, generated by Hoverlab — hoverlab.dev/tools/shadcn
Replace the :root, .dark and @theme inline blocks in your globals.css. */
:root {
--radius: 0.625rem;
--background: oklch(1.000 0.000 0.0);
--foreground: oklch(0.145 0.006 250.0);
--card: oklch(1.000 0.000 0.0);
--card-foreground: oklch(0.145 0.006 250.0);
--popover: oklch(1.000 0.000 0.0);
--popover-foreground: oklch(0.145 0.006 250.0);
--primary: oklch(0.520 0.190 250.0);
--primary-foreground: oklch(0.985 0.006 250.0);
--ring: oklch(0.708 0.076 250.0);
--secondary: oklch(0.970 0.006 250.0);
--secondary-foreground: oklch(0.205 0.006 250.0);
--muted: oklch(0.970 0.006 250.0);
--muted-foreground: oklch(0.545 0.006 250.0);
--accent: oklch(0.970 0.006 250.0);
--accent-foreground: oklch(0.205 0.006 250.0);
--destructive: oklch(0.577 0.245 27.3);
--destructive-foreground: oklch(0.985 0.000 0.0);
--border: oklch(0.922 0.006 250.0);
--input: oklch(0.922 0.006 250.0);
--chart-1: oklch(0.660 0.200 250.0);
--chart-2: oklch(0.400 0.200 322.0);
--chart-3: oklch(0.530 0.200 34.0);
--chart-4: oklch(0.790 0.200 106.0);
--chart-5: oklch(0.920 0.200 178.0);
--sidebar: oklch(0.985 0.006 250.0);
--sidebar-foreground: oklch(0.145 0.006 250.0);
--sidebar-primary: oklch(0.520 0.190 250.0);
--sidebar-primary-foreground: oklch(0.985 0.006 250.0);
--sidebar-accent: oklch(0.970 0.006 250.0);
--sidebar-accent-foreground: oklch(0.205 0.006 250.0);
--sidebar-border: oklch(0.922 0.006 250.0);
--sidebar-ring: oklch(0.708 0.076 250.0);
}
.dark {
--background: oklch(0.145 0.006 250.0);
--foreground: oklch(0.985 0.006 250.0);
--card: oklch(0.205 0.006 250.0);
--card-foreground: oklch(0.985 0.006 250.0);
--popover: oklch(0.205 0.006 250.0);
--popover-foreground: oklch(0.985 0.006 250.0);
--primary: oklch(0.720 0.190 250.0);
--primary-foreground: oklch(0.145 0.006 250.0);
--ring: oklch(0.556 0.076 250.0);
--secondary: oklch(0.269 0.006 250.0);
--secondary-foreground: oklch(0.985 0.006 250.0);
--muted: oklch(0.269 0.006 250.0);
--muted-foreground: oklch(0.708 0.006 250.0);
--accent: oklch(0.269 0.006 250.0);
--accent-foreground: oklch(0.985 0.006 250.0);
--destructive: oklch(0.704 0.191 22.2);
--destructive-foreground: oklch(0.145 0.006 250.0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--chart-1: oklch(0.680 0.200 250.0);
--chart-2: oklch(0.440 0.200 322.0);
--chart-3: oklch(0.560 0.200 34.0);
--chart-4: oklch(0.800 0.200 106.0);
--chart-5: oklch(0.920 0.200 178.0);
--sidebar: oklch(0.205 0.006 250.0);
--sidebar-foreground: oklch(0.985 0.006 250.0);
--sidebar-primary: oklch(0.720 0.190 250.0);
--sidebar-primary-foreground: oklch(0.145 0.006 250.0);
--sidebar-accent: oklch(0.269 0.006 250.0);
--sidebar-accent-foreground: oklch(0.985 0.006 250.0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0.076 250.0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-ring: var(--ring);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
Replace the :root, .dark and @theme inline blocks. The last one is the part people forget — in Tailwind v4 the variables alone give you nothing, because bg-primary only exists once that block maps it.
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "theme",
"type": "registry:theme",
"title": "Custom theme",
"description": "A light and dark shadcn/ui token set, generated at hoverlab.dev/tools/shadcn.",
"cssVars": {
"light": {
"radius": "0.625rem",
"background": "oklch(1.000 0.000 0.0)",
"foreground": "oklch(0.145 0.006 250.0)",
"card": "oklch(1.000 0.000 0.0)",
"card-foreground": "oklch(0.145 0.006 250.0)",
"popover": "oklch(1.000 0.000 0.0)",
"popover-foreground": "oklch(0.145 0.006 250.0)",
"primary": "oklch(0.520 0.190 250.0)",
"primary-foreground": "oklch(0.985 0.006 250.0)",
"ring": "oklch(0.708 0.076 250.0)",
"secondary": "oklch(0.970 0.006 250.0)",
"secondary-foreground": "oklch(0.205 0.006 250.0)",
"muted": "oklch(0.970 0.006 250.0)",
"muted-foreground": "oklch(0.545 0.006 250.0)",
"accent": "oklch(0.970 0.006 250.0)",
"accent-foreground": "oklch(0.205 0.006 250.0)",
"destructive": "oklch(0.577 0.245 27.3)",
"destructive-foreground": "oklch(0.985 0.000 0.0)",
"border": "oklch(0.922 0.006 250.0)",
"input": "oklch(0.922 0.006 250.0)",
"chart-1": "oklch(0.660 0.200 250.0)",
"chart-2": "oklch(0.400 0.200 322.0)",
"chart-3": "oklch(0.530 0.200 34.0)",
"chart-4": "oklch(0.790 0.200 106.0)",
"chart-5": "oklch(0.920 0.200 178.0)",
"sidebar": "oklch(0.985 0.006 250.0)",
"sidebar-foreground": "oklch(0.145 0.006 250.0)",
"sidebar-primary": "oklch(0.520 0.190 250.0)",
"sidebar-primary-foreground": "oklch(0.985 0.006 250.0)",
"sidebar-accent": "oklch(0.970 0.006 250.0)",
"sidebar-accent-foreground": "oklch(0.205 0.006 250.0)",
"sidebar-border": "oklch(0.922 0.006 250.0)",
"sidebar-ring": "oklch(0.708 0.076 250.0)"
},
"dark": {
"background": "oklch(0.145 0.006 250.0)",
"foreground": "oklch(0.985 0.006 250.0)",
"card": "oklch(0.205 0.006 250.0)",
"card-foreground": "oklch(0.985 0.006 250.0)",
"popover": "oklch(0.205 0.006 250.0)",
"popover-foreground": "oklch(0.985 0.006 250.0)",
"primary": "oklch(0.720 0.190 250.0)",
"primary-foreground": "oklch(0.145 0.006 250.0)",
"ring": "oklch(0.556 0.076 250.0)",
"secondary": "oklch(0.269 0.006 250.0)",
"secondary-foreground": "oklch(0.985 0.006 250.0)",
"muted": "oklch(0.269 0.006 250.0)",
"muted-foreground": "oklch(0.708 0.006 250.0)",
"accent": "oklch(0.269 0.006 250.0)",
"accent-foreground": "oklch(0.985 0.006 250.0)",
"destructive": "oklch(0.704 0.191 22.2)",
"destructive-foreground": "oklch(0.145 0.006 250.0)",
"border": "oklch(1 0 0 / 10%)",
"input": "oklch(1 0 0 / 15%)",
"chart-1": "oklch(0.680 0.200 250.0)",
"chart-2": "oklch(0.440 0.200 322.0)",
"chart-3": "oklch(0.560 0.200 34.0)",
"chart-4": "oklch(0.800 0.200 106.0)",
"chart-5": "oklch(0.920 0.200 178.0)",
"sidebar": "oklch(0.205 0.006 250.0)",
"sidebar-foreground": "oklch(0.985 0.006 250.0)",
"sidebar-primary": "oklch(0.720 0.190 250.0)",
"sidebar-primary-foreground": "oklch(0.145 0.006 250.0)",
"sidebar-accent": "oklch(0.269 0.006 250.0)",
"sidebar-accent-foreground": "oklch(0.985 0.006 250.0)",
"sidebar-border": "oklch(1 0 0 / 10%)",
"sidebar-ring": "oklch(0.556 0.076 250.0)"
}
}
}What the install command above fetches. Commit it to your own repo if you would rather serve the theme yourself — it is a plain registry:theme item and the CLI does not care where it comes from.
Now use it on something
Every effect, block, page and template in the catalog is styled against tokens shaped exactly like these — so they take your values rather than fighting them.
npx shadcn@latest add https://hoverlab.dev/r/hoverlab.json
- Token GeneratorBuild the CSS custom properties every block in the catalog is styled against — a full light and dark set in the shadcn convention. Colour maths in OKLCH, so one lightness scale works across every hue.
- Live Palette PreviewYour palette on real components — a navbar, a hero, pricing, a dashboard, a footer — repainted as you drag, in both themes, with the contrast of every token pair checked while you do it. Not swatches next to lorem ipsum.
- Grid GeneratorPaint a page layout onto a grid and copy the CSS, with grid-template-areas checked for the rectangle rule as you go — the rule that silently voids the whole declaration when you break it. Explicit tracks or a responsive auto-fit wall.