Styling Extension
@chroma/style — design tokens, utility CSS, and cva presets integrated with Chroma's cx and props.class.
@chroma/style is the first-party styling plugin for Chroma apps. It ships design tokens (tokens.css), atomic utility classes (utilities.css), and shadcn-style component presets (variants.js) that compose with the engine's WASM cva and cx helpers.
The plugin folder name is chroma-style; the npm-style package id is @chroma/style. When you run chroma new, it is vendored into plugins/chroma-style/ from the canonical Chroma-Style/ workspace package. CSS and variant presets are mirrored to Chroma/extensions/styling/ for Chroma Desktop — see Canonical source below.
Relationship to cva and cx
Chroma's core (Architecture → variants.rs) exports two pure class-name utilities:
| Export | Role |
|---|---|
cva(config) | Returns (props) => string — resolves base, variant groups, compound variants, defaults, then props.class / props.className |
cx(...parts) | Flattens and joins class fragments (strings, arrays, falsy filtering) |
@chroma/style does not reimplement either helper. It:
- Injects CSS so utility class names resolve to real styles.
- Builds preset variant configs (
button,badge,card,input) that reference those utility names. - Expects you to pass the engine's
cvaintobootstrapStyle({ cva, cx })orcreateVariants(cva)so presets behave identically to your owncvaconfigs.
For the resolution order of cva (including props.class), see Components & Variants.
Setup
1. Declare the plugin
Add chroma-style to chroma.json (included in all chroma new templates):
{
"name": "my-app",
"mode": "dynamic",
"plugins": ["chroma-fs", "chroma-router", "chroma-style"]
}2. Loader injects CSS
Templates ship plugins/loader.js, which calls injectStyles() when the plugin is listed. See the full loader pattern in Extensions & Plugins.
if (name === "chroma-style") {
const mod = await import("./chroma-style/index.js");
mod.injectStyles();
loaded.style = mod;
}3. Bootstrap presets in main.js
Pass the engine's cva and cx once at startup. bootstrapStyle() is idempotent — safe even if the loader already injected CSS.
import { render, cva, cx } from "../pkg/chroma.js";
import { loadPlugins } from "../plugins/loader.js";
const plugins = await loadPlugins();
if (plugins.style) {
globalThis.__chromaStyle = plugins.style.bootstrapStyle({ cva, cx });
}
render(App, document.getElementById("app"));Destructure from globalThis.__chromaStyle (or import the return value directly):
const { tw, presets } = globalThis.__chromaStyle;Design tokens
All colors, radii, shadows, and type scales are CSS custom properties in tokens.css. Utility classes and variants.js read these variables — retheming an app means editing tokens.css only.
Semantic colors
| Token | Light (:root) | Purpose |
|---|---|---|
--background / --foreground | #ffffff / #09090b | Page surface and default text |
--card / --card-foreground | #ffffff / #09090b | Elevated surfaces |
--primary / --primary-foreground | #18181b / #fafafa | Primary actions |
--secondary / --secondary-foreground | #f4f4f5 / #18181b | Secondary actions |
--muted / --muted-foreground | #f4f4f5 / #71717a | Subtle backgrounds and helper text |
--accent / --accent-foreground | #f4f4f5 / #18181b | Highlights |
--destructive / --destructive-foreground | #ef4444 / #fafafa | Errors and destructive actions |
--border, --input, --ring | #e4e4e7 … | Borders, inputs, focus ring |
Radius, shadow, typography scales
| Category | Variables |
|---|---|
| Radius | --radius-none … --radius-full (0 → 9999px) |
| Shadow | --shadow-sm, --shadow-md, --shadow-lg, --shadow-xl |
| Font | --font-sans |
| Text size | --text-xs … --text-2xl (+ matching --text-*-leading) |
Light / dark theme
Three modes, in priority order:
- Explicit dark — set
data-theme="dark"on<html>(or any ancestor). - Explicit light — set
data-theme="light"(add a:root[data-theme="light"]block if you customize tokens). - System — when no
data-themeis set,@media (prefers-color-scheme: dark)applies the dark palette to:root.
<html data-theme="dark">Toggle at runtime:
document.documentElement.dataset.theme =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";Utility classes
utilities.css maps token variables to atomic classes — a Tailwind-style subset scoped to what cva() configs typically compose. This is intentionally basic, not a full Tailwind port.
Background
bg-background, bg-foreground, bg-card, bg-primary, bg-secondary, bg-muted, bg-accent, bg-destructive, bg-transparent
Text
text-foreground, text-card-foreground, text-primary, text-primary-foreground, text-secondary-foreground, text-muted-foreground, text-accent-foreground, text-destructive, text-destructive-foreground, text-center
Typography
font-sans, text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, font-normal, font-medium, font-semibold, font-bold
Borders
border, border-0, border-2, border-input, border-destructive, border-transparent
Rounded
rounded-none, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-full
Shadows
shadow-none, shadow-sm, shadow-md, shadow-lg, shadow-xl
Layout and spacing
flex, inline-flex, flex-col, items-center, justify-center, justify-between, gap-1, gap-2, gap-4, w-full, p-2, p-4, px-2, px-3, px-4, py-1, py-2, m-0, mt-2, mt-4, mb-2, mb-4
Interaction
transition-colors, focus-ring, disabled:opacity-50, disabled:pointer-events-none
cva presets
variants.js exports createVariants(cva), which returns { button, badge, card, input }. Each preset is a cva config using the utility classes above.
import { cva, cx } from "../pkg/chroma.js";
import { createVariants } from "./plugins/chroma-style/variants.js";
const presets = createVariants(cva);Or via bootstrap:
const { presets } = bootstrapStyle({ cva, cx });presets.button
| Variant group | Options | Default |
|---|---|---|
variant | primary, secondary, outline, ghost, destructive | primary |
size | sm, md, lg | md |
presets.badge
| Variant group | Options | Default |
|---|---|---|
variant | default, secondary, outline, destructive | default |
presets.card
| Variant group | Options | Default |
|---|---|---|
padding | none, sm, md | md |
shadow | none, sm, md, lg | sm |
presets.input
| Variant group | Options | Default |
|---|---|---|
invalid | true (adds border-destructive) | — |
API reference
All exports live in plugins/chroma-style/index.js (re-exported from variants.js where noted).
bootstrapStyle(engine?)
One-call setup: inject CSS, return helpers.
bootstrapStyle({ cva, cx });
// → { tw, presets, utilityClasses, injectStyles, createVariants }| Field | Type | Notes |
|---|---|---|
tw | (...parts) => string | Thin wrapper around engine cx (or a standalone join if cx omitted) |
presets | { button, badge, card, input } | null if cva was not passed |
utilityClasses | Record<string, string[]> | All class names grouped by category |
injectStyles | () => void | Same function, for manual re-call |
createVariants | (cva) => presets | Alias to build presets without full bootstrap |
injectStyles()
Injects <link rel="stylesheet"> tags for tokens.css and utilities.css into document.head. Idempotent — skips sheets already marked with data-chroma-plugin="chroma-style:…". No-op when document is undefined (SSR guard).
createVariants(cva) / createPresets(cva)
Build preset resolvers from the engine's cva. createPresets is deprecated alias for createVariants.
createTw(cxFn?)
Returns a class merger. Uses engine cx when provided; otherwise joins truthy parts with spaces.
utilityClasses
Static object listing every utility and preset name — useful for docs generators, IDE snippets, or validation:
import { utilityClasses } from "./plugins/chroma-style/index.js";
utilityClasses.background; // ["bg-background", "bg-foreground", …]
utilityClasses.presets; // ["button", "badge", "card", "input"]Examples
Reusable styled components
Wrap presets in plain e() functions — same pattern as Components & Variants:
const { tw, presets } = globalThis.__chromaStyle;
function SaveButton(props) {
return e(
"button",
{ class: presets.button(props), onClick: props.onClick, disabled: props.disabled },
props.children,
);
}
e(SaveButton, { variant: "primary", size: "lg" }, "Save");
e(SaveButton, { variant: "outline", size: "sm", class: "mt-4" }, "Cancel");The last call appends mt-4 via props.class — resolved last by cva.
Style showcase (cards, badges, inputs)
From Chroma/example/index.html — copy-paste starting point:
function StyleShowcase() {
return e(
"section",
{ class: presets.card({ shadow: "md", padding: "md" }) },
e("h2", null, "@chroma/style — tw + presets"),
e("p", { class: tw("text-muted-foreground", "text-sm", "mb-4") },
"Utility CSS + cva presets from the plugin."),
e("div", { class: tw("flex", "gap-2", "mb-4") },
e("button", { class: presets.button({ variant: "primary", size: "sm" }) }, "Primary"),
e("button", { class: presets.button({ variant: "secondary", size: "sm" }) }, "Secondary"),
e("button", { class: presets.button({ variant: "outline", size: "sm" }) }, "Outline"),
e("button", { class: presets.button({ variant: "destructive", size: "sm" }) }, "Destructive"),
),
e("div", { class: tw("flex", "gap-2", "mb-4") },
e("span", { class: presets.badge({ variant: "default" }) }, "Badge"),
e("span", { class: presets.badge({ variant: "secondary" }) }, "Secondary"),
e("span", { class: presets.badge({ variant: "outline" }) }, "Outline"),
),
e("input", { class: presets.input(), placeholder: "Input preset", type: "text" }),
);
}Combining presets with manual classes
Use cx or tw to merge preset output with one-off utilities:
e("div", {
class: cx(presets.card({ padding: "md" }), "mt-4 shadow-lg"),
});
e("p", {
class: tw("text-muted-foreground", "text-sm", isActive() && "text-primary"),
});Custom cva on top of utilities
You are not limited to presets — point your own cva configs at the same utility names:
const alertClass = cva({
base: "rounded-md border px-4 py-2 text-sm",
variants: {
tone: {
info: "bg-secondary text-secondary-foreground border-transparent",
error: "bg-destructive text-destructive-foreground border-transparent",
},
},
defaultVariants: { tone: "info" },
});
e("div", { class: alertClass({ tone: "error", class: "mt-2" }) }, "Something went wrong.");Manual CSS import (no loader)
Without plugins/loader.js:
import { injectStyles, bootstrapStyle } from "./plugins/chroma-style/index.js";
import { cva, cx } from "../pkg/chroma.js";
injectStyles();
const { tw, presets } = bootstrapStyle({ cva, cx });Or link stylesheets from index.html:
<link rel="stylesheet" href="./plugins/chroma-style/tokens.css" />
<link rel="stylesheet" href="./plugins/chroma-style/utilities.css" />Integration with props.class
Chroma's WASM cva always appends props.class and props.className after variant resolution. Presets from @chroma/style inherit this behavior because they are built with the same engine cva.
// Inside a component — pass props through to the preset resolver
function Card(props) {
return e("div", { class: presets.card(props) }, props.children);
}
// Caller adds layout utilities without forking the preset
e(Card, { padding: "md", shadow: "lg", class: "mt-4 w-full" }, content);Use cx / tw when merging outside a single cva call (multiple resolvers or raw strings). See Components & Variants → Resolution order for the full cva pipeline.
Canonical source
The design system exists in two shapes:
| Location | Used by | Contents |
|---|---|---|
Chroma-Style/ | Canonical @chroma/style package | chroma.plugin.json, index.js, tokens.css, utilities.css, variants.js |
Chroma/cli/plugins/chroma-style/ | chroma new, chroma install | Synced copy vendored into apps |
Chroma/extensions/styling/ | Chroma Desktop shell | CSS + variants.js only (extension.json, no index.js) |
License: GNU GPL version 3 — see Chroma-Style and Licencia GPLv3. Canonical git remote: github.com/Targ-Apps/Chroma-Style.
Edit Chroma-Style/, then sync to every vendored destination:
./scripts/sync-chroma-style.sh./scripts/build-chroma-cli.sh runs this sync automatically before assembling Chroma-CLI/.
Hook points: bootstrap and styles — documented in Plugin Standard and Extensions & Plugins.
Related docs
- Components & Variants —
cva,cx, andprops.classresolution - Architecture — WASM
variants.rs, extension overview - Extensions & Plugins — manifest, loader pattern, hook points
- Plugin Standard —
chroma.plugin.json, lifecycle, authoring checklist - CLI —
chroma new, plugin vendoring,build-chroma-cli.sh - Routing — often combined with styled API demo cards in examples
- Examples — full hook tour in
Chroma/example/index.html
Scope
@chroma/style is intentionally basic — colors, borders, rounded corners, shadows, typography, and minimal layout/spacing sufficient for internal apps and prototypes. For marketing sites or large design systems, bring your own CSS or an external framework.