Skip to content
Kinetixui

Theming

KinetixUI uses a semantic CSS variable contract. Components reference semantic names only; the mapping to real colours lives in one place.

The contract#

VariableLightFigma source
--background#ffffffsurfaceContainerLowest
--foreground#050c11onSurface
--primary#1d4ed8action blue (replaces Figma Primay navy)
--primary-foreground#f0f7ffOn Primary
--secondary#c7cfc7secondaryContainer (deepened for surface contrast)
--muted / --muted-foreground#f6f6f6 / #6d6d6dsurfaceContainer / onSurfaceVariant
--accent#f0f7ffLightBlue
--destructive#ec5047error
--border / --input#92b2c8outline
--ring#1d4ed8focus outline (tracks --primary)
--radius8pxButton

--card, --popover, --ring, the neutral ramp, and the entire dark theme are synthesized — Figma does not define them yet. Every synthesized value is listed in the repo's TOKENS.md.

Both themes#

Light and dark are two independent palettes — not one value set flipped. Most light roles are aliased from Figma; --primary / --ring are an action blue (#1d4ed8 light, #60a5fa dark, each picked to clear WCAG AA on its own surface); the rest of dark is synthesized.

light

Card surface with muted foreground text.

dark

Card surface with muted foreground text.

Retheme#

Override the semantic variables after importing the contract. Primitives stay put; only the mapping changes:

@import "@kinetixui/tokens/css";
 
:root {
  --primary: var(--green-500);
  --primary-foreground: var(--green-50);
  --radius: 12px;
}

primary is doing several jobs in a shadcn-style contract: the fill of a button, the colour of a link, the focus ring, and — in a lot of brands — the brand colour itself. Those diverge as soon as your brand colour isn't a good button colour (KinetixUI's own navy wasn't, which is why primary is azure). Four role tokens sit on top of it so you can split them:

TokenMeaningDefault
--action / --action-foregroundthe interactive fill: buttons, checkboxes, selected states, spinnersvar(--primary) / var(--primary-foreground)
--linkLink button, inline linksvar(--primary)
--focusfocus indicatorvar(--ring)
--brand / --brand-foregroundidentity: logos, headers, brand surfaces — not a control colourthe navy blue.500

Because action, link and focus default to primary / ring, a theme that only sets --primary (everything above, and everything the CLI's theme command writes) keeps working unchanged. Override a role on its own to split it off:

:root {
  --primary: var(--green-500);        /* everything follows… */
  --action: var(--azure-700);         /* …except buttons and controls, which stay azure */
  --brand: var(--green-700);
}

Components read the role tokens (bg-action, text-link), not primary, so overriding --primary still recolours them via the default and overriding --action recolours only the interactive layer. Contrast is on you when you split them: kinetixui theme build reports action / action-foreground and brand / brand-foreground when you set them.

Hover and pressed. On the web the Button derives its states from action (bg-action/90, bg-action/85), so they follow whatever you set. The contract also carries explicit --action-hover / --action-pressed values — the same two colours, precomputed over the page background — for the SwiftUI, Compose and Flutter ports, which have no alpha-derivation to lean on. pnpm check:contrast asserts the explicit values equal the derivation and that text on them clears AA.

A theme generated in Create derives the same two states the same way, with one constraint on top: the label has to stay readable on all three fills. action-foreground is chosen against action, action-hover and action-pressed together rather than the resting colour alone, and where no foreground can carry the full 90% / 85% move, both states scale back in step until it can. Some brand colours therefore get a slightly subtler hover than the shipped blue does — the alternative was a button label at 3.4:1 while pressed, which is what this used to do.

Native. The SwiftUI, Compose and Flutter components read the same role tokens: KinetixColors has action, actionForeground, actionHover, actionPressed, link, focus, brand and brandForeground alongside primary / ring, and the components use the role fields (the Button's Link variant reads link; focus rings read focus). The primary Button uses actionHover on pointer hover and actionPressed while pressed; other components keep the platform's own interaction feedback. primary remains as the source they default to.

Upgrading: components that use bg-action / text-link need the matching @kinetixui/ui/tailwind.config preset (it defines the new colours) — update the package when you re-add components with the CLI.

Primitive ramps#

Seven ramps (azure, blue, green, taupe, cream, amber, red) plus a synthesized neutral, each 0–1000. Browse them on the Colors page.

Building a theme visually#

Create generates a theme from a handful of choices rather than a list of hex values. Pick a theme colour and it derives the brand, action, link and focus roles from it, along with the hover and pressed states and a foreground that clears AA — in OKLCH, so the same rule produces the same perceptual step on every hue. A separate neutral control sets the personality of the greys, and radius and surface presets write the --radius-* and --shadow-* variables documented above.

Its Export panel then generates that design for one of four targets: web CSS, SwiftUI, Jetpack Compose or Flutter. The web CSS is an override block containing only what differs from the shipped theme, with a :root and a .dark section — one configuration describes both appearances. The three native targets are a KinetixColors pair each, light and dark. Manual values set in the Advanced panel beat everything generated, and a contrast failure you introduce there is reported rather than quietly corrected.

Each target also shows the kinetixui preset … command that writes the same file, because the CLI and the workspace run the same exporter.

Radius and surface reach SwiftUI and stop there. KinetixRadii and KinetixElevations are part of its theme, its components read them, and preset swiftui writes them — so a design's corners and shadows arrive intact, minus shadow spread, which SwiftUI has no equivalent for. Compose and Flutter still read generated constants rather than a runtime theme, so there is nowhere for either to land on those two. Colours and the chart palette travel everywhere. kinetixui theme build is a separate, older path — a CSV of literal colours compiled to CSS — and is web-only; see the CLI page for its scope.

Presets#

A configuration can be saved as a preset: a short code beginning KX1_ that describes the design and nothing else. It carries the theme colour, the neutral, the radius, the surface treatment, the chart palette and any manual overrides — not the generated colours, not the CSS, and not which appearance or demo scene you happened to be looking at. Those are regenerated from the preset by the same engine that produced them the first time.

Share copies a /create?preset=KX1_… link that reopens the workspace with that design. Copy preset copies the bare code, which is what kinetixui preset decode reads and kinetixui preset css resolves into this same stylesheet without a browser — and preset swiftui, preset compose and preset flutter resolve into native theme files.

The workspace's Export panel is the same four targets in the browser: one design, one resolved theme, and the exporter for whichever target is selected. A preset stays a different artifact from any of them — a preset is the design, generated code is one rendering of it.

The preview is always web. Selecting SwiftUI, Jetpack Compose or Flutter changes what Export generates, not what is drawn: those targets carry colours, and radius and surface apply on the web only, because no native package has a runtime token for them.

The version is in the prefix, so a code made today stays readable, and a future format is refused with an explanation rather than misread. A preset can only name tokens from the contract above — it is structured configuration, never arbitrary CSS.

Presets reach web CSS and all three native colour contracts. preset swiftui, preset compose and preset flutter resolve a preset into a KinetixColors pair for SwiftUI, Compose and Flutter. Colours only in all three: a design's radius and surface treatment apply on the web and have no runtime token to land on natively. Flutter carries the whole semantic contract; Compose is missing fields for input, ring and tertiary-foreground, and its generated file says so.