KKiotaPay UI
Browse documentation

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

ItemTypeUse withDoes
kiota-themeregistry:themeaddThe whole design system: brand colours, state ramps, radius, fonts, money helpers
kiota-styleregistry:styleinitSets 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.json

Given 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:

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.json

From 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.svg

Then 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)" }
  }
}

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:build

Never 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

TokenUtilityRole
--primarybg-primaryPrimary brand colour; shadcn's default button and badge
--primary-foregroundtext-primary-foregroundText on --primary
--primary-lightbg-primary-lightPale tint for surfaces, hovers, selected rows
--secondarybg-secondarySecond brand colour
--secondary-foregroundtext-secondary-foregroundText on --secondary
--secondary-lightbg-secondary-lightPale tint of the secondary
--accentbg-accentQuiet hover/active surface
--ringring-ringFocus ring
--destructivebg-destructiveAlways published equal to --danger, so the two cannot drift

State

RampSolidText on solidTint surface
successbg-successtext-success-foregroundbg-success-light
warningbg-warningtext-warning-foregroundbg-warning-light
dangerbg-dangertext-danger-foregroundbg-danger-light
infobg-infotext-info-foregroundbg-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