Theming
A registry can distribute design tokens, not just components. Installing a theme item merges its variables into the host project's stylesheet — replacing variables that already exist and adding the ones that do not, while leaving everything else untouched.
Two items, two jobs
| Item | Type | Use with | Does |
|---|---|---|---|
kiota-theme | registry:theme | add | The whole design system: brand colours, state ramps, radius, fonts, money helpers |
kiota-style | registry:style | init | Sets up a new project: full shadcn base, the theme, and every component |
Theming an existing project
pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/kiota-theme.jsonnpx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/kiota-theme.jsonyarn dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/kiota-theme.jsonbunx --bun shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/kiota-theme.jsonGiven a project whose stylesheet already looks like this:
:root {
--radius: 1.5rem;
--background: oklch(1 0 0);
--primary: oklch(0.4 0.2 300);
--my-own-token: #ff0000;
}installing produces:
:root {
- --radius: 1.5rem;
+ --radius: 0.625rem;
--background: oklch(1 0 0);
- --primary: oklch(0.4 0.2 300);
+ --primary: oklch(0.52 0.13 165);
+ --primary-foreground: oklch(0.985 0 0);
--my-own-token: #ff0000;
+ --accent: oklch(0.96 0.02 165);
+ --accent-foreground: oklch(0.35 0.09 165);
+ --ring: oklch(0.52 0.13 165);
+ --success: oklch(0.52 0.13 155);
+ /* …the rest of the state and brand tokens */
}Three behaviours worth noting:
--radiusand--primarywere replaced — the theme owns them.--accent,--ringand the state ramps were added — they were missing.--backgroundand--my-own-tokenwere left alone — the theme does not name them, so it does not touch them.
The same merge happens inside .dark. The CLI also generates matching @theme inline mappings and a @custom-variant dark if the project lacks one.
kiota-theme names only the tokens KiotaPay brands: primary, secondary, accent, ring, radius, fonts, and
--destructive as an alias of --danger. Everything else — background, card, muted, popover, border — stays at
whatever base colour the host project chose, so the theme layers onto a slate, zinc or stone project without
flattening it.
Setting up a new project
shadcn init accepts a registry item as its argument, so a single command can configure a fresh project with the whole design system:
npx shadcn@latest init https://kiotapay-ui-staging.staging.kiotapay.com/r/kiota-style.jsonFrom a stylesheet containing nothing but @import 'tailwindcss', that writes components.json, lays down the complete shadcn token set (card, popover, muted, destructive, chart and sidebar tokens included), applies the KiotaPay brand overrides on top, and installs every component along with the shadcn primitives they need.
Because kiota-style does not set extends: "none", it builds on the shadcn defaults rather than replacing them. A style that starts from a blank slate instead would declare:
{
"extends": "none",
"name": "kiota-style",
"type": "registry:style"
}That suppresses the shadcn base entirely, leaving only the variables the item itself defines — use it only if you intend to specify every token yourself.
init does not add the @kiota namespace to the generated components.json. Add it manually afterwards if you want
the @kiota/… shorthand — see Installation.
Brand assets
Brand assets are not installed by the registry — add them yourself. Create a theme-files/ directory in your project's public/ and download the assets into it from the asset endpoint:
mkdir -p public/theme-files
curl -O --output-dir public/theme-files https://kiotapay-ui-staging.staging.kiotapay.com/theme-files/kiota-logo.svg
curl -O --output-dir public/theme-files https://kiotapay-ui-staging.staging.kiotapay.com/theme-files/kiota-wordmark.svgThen reference them as ordinary public assets:
<img src="/theme-files/kiota-logo.svg" alt="KiotaPay" />The endpoint is configurable through NEXT_PUBLIC_THEME_FILES_URL, so the commands above always point at wherever the
assets are currently hosted. It defaults to this site's own /theme-files/.
Assets stay out of the registry deliberately. Item contents travel as UTF-8 JSON strings, so text like SVG survives but anything binary — fonts, PNGs, icons — is corrupted in transit with no error from shadcn build. Serving them over HTTP sidesteps the problem entirely and lets you update artwork without anyone re-running an install.
What a theme item can carry
{
"name": "kiota-theme",
"type": "registry:theme",
"cssVars": {
"theme": { "font-sans": "Inter, ui-sans-serif, system-ui, sans-serif" },
"light": { "radius": "0.625rem", "primary": "oklch(0.52 0.13 165)" },
"dark": { "primary": "oklch(0.75 0.15 165)" }
}
}cssVars.themegoes into@theme— Tailwind-level tokens such as fonts, spacing and easings.cssVars.lightgoes into:root;cssVars.darkgoes into.dark.- Names are written without the leading
--.
For anything that is not a variable — keyframes, base-layer rules, custom utilities — use the css field instead:
{
"css": {
"@keyframes kiota-pulse": {
"0%, 100%": { "opacity": "1" },
"50%": { "opacity": "0.4" }
},
"@layer base": {
"h1": { "font-size": "var(--text-2xl)" }
},
"@utility tabular": {
"font-variant-numeric": "tabular-nums"
}
}
}Changing the brand colours
app/globals.css is the only place a token value is written. It carries two marked blocks, and npm run registry:build parses them into the cssVars of the matching registry items:
/* registry:begin kiota-theme */
:root {
--primary: oklch(0.52 0.13 165);
--primary-light: oklch(0.95 0.04 165);
}
.dark {
--primary: oklch(0.75 0.15 165);
--primary-light: oklch(0.28 0.05 165);
}
/* registry:end kiota-theme */Change a value, run the build, done — the docs previews and the published item move together:
npm run registry:buildNever hand-edit the generated cssVars in a registry catalog — the next build overwrites them. Adding a token means
adding it to the marked block in globals.css, plus a --color-* line in @theme inline so the Tailwind utility
exists locally. A block naming an item that does not exist fails the build rather than silently shipping nothing.
One token may alias another in globals.css — --destructive: var(--danger) keeps shadcn's destructive variants and
our danger surfaces from ever diverging. The generator resolves that indirection to a literal before publishing,
because the CLI mishandles a var() value when it writes a consumer's @theme block: --destructive: var(--danger)
came out the other side as the circular --destructive: var(--destructive), which resolves to nothing and silently
kills the bg-destructive utility.
Token reference
Everything below ships in the single kiota-theme item. The brand group overrides shadcn's own semantics, so a consumer's Button, Badge and focus rings adopt KiotaPay colours with no code changes.
Brand
| Token | Utility | Role |
|---|---|---|
--primary | bg-primary | Primary brand colour; shadcn's default button and badge |
--primary-foreground | text-primary-foreground | Text on --primary |
--primary-light | bg-primary-light | Pale tint for surfaces, hovers, selected rows |
--secondary | bg-secondary | Second brand colour |
--secondary-foreground | text-secondary-foreground | Text on --secondary |
--secondary-light | bg-secondary-light | Pale tint of the secondary |
--accent | bg-accent | Quiet hover/active surface |
--ring | ring-ring | Focus ring |
--destructive | bg-destructive | Always published equal to --danger, so the two cannot drift |
State
| Ramp | Solid | Text on solid | Tint surface |
|---|---|---|---|
| success | bg-success | text-success-foreground | bg-success-light |
| warning | bg-warning | text-warning-foreground | bg-warning-light |
| danger | bg-danger | text-danger-foreground | bg-danger-light |
| info | bg-info | text-info-foreground | bg-info-light |
The -light tints exist in both themes — in dark mode a "tint" is a dark, desaturated version of the hue, not a pale one. Pair a tint surface with the solid as text (bg-success-light text-success); the two are far enough apart in lightness to clear 4.5:1 in either theme.
Overriding --secondary means <Button variant="secondary"> renders in your second brand colour rather than
shadcn's quiet grey surface. That is usually what you want from a branded system, but it is a visible change to every
secondary button. To keep shadcn's behaviour, drop --secondary from the kiota-theme block and use
bg-secondary-light explicitly where you want the brand tint.
cssVars apply at install time only. A fresh install always writes current values, but re-installing into a
project that already has those variables does not reliably update them — in testing the same token updated on one run
and stayed stale on another. Tell consumers to edit the variables in their stylesheet directly rather than expecting a
re-run to propagate a palette change. See Updating.
Limits
- No partial re-theme. When a re-run does apply, it applies every token in the item at once — you cannot adopt one colour and leave the rest. Tokens the item does not name are untouched either way.
- Fonts are declarations, not downloads. Setting
--font-sanstoInterdoes not install Inter. Ship the font throughdependenciesand document the loading step, or stick to a system stack. registry:styleis forinit. Runningaddon a style item works but skips the base-style setup that makes it useful.