Targ Apps Docs
Chroma

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:

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

  1. Injects CSS so utility class names resolve to real styles.
  2. Builds preset variant configs (button, badge, card, input) that reference those utility names.
  3. Expects you to pass the engine's cva into bootstrapStyle({ cva, cx }) or createVariants(cva) so presets behave identically to your own cva configs.

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

TokenLight (:root)Purpose
--background / --foreground#ffffff / #09090bPage surface and default text
--card / --card-foreground#ffffff / #09090bElevated surfaces
--primary / --primary-foreground#18181b / #fafafaPrimary actions
--secondary / --secondary-foreground#f4f4f5 / #18181bSecondary actions
--muted / --muted-foreground#f4f4f5 / #71717aSubtle backgrounds and helper text
--accent / --accent-foreground#f4f4f5 / #18181bHighlights
--destructive / --destructive-foreground#ef4444 / #fafafaErrors and destructive actions
--border, --input, --ring#e4e4e7 …Borders, inputs, focus ring

Radius, shadow, typography scales

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

  1. Explicit dark — set data-theme="dark" on <html> (or any ancestor).
  2. Explicit light — set data-theme="light" (add a :root[data-theme="light"] block if you customize tokens).
  3. System — when no data-theme is 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 groupOptionsDefault
variantprimary, secondary, outline, ghost, destructiveprimary
sizesm, md, lgmd

presets.badge

Variant groupOptionsDefault
variantdefault, secondary, outline, destructivedefault

presets.card

Variant groupOptionsDefault
paddingnone, sm, mdmd
shadownone, sm, md, lgsm

presets.input

Variant groupOptionsDefault
invalidtrue (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 }
FieldTypeNotes
tw(...parts) => stringThin wrapper around engine cx (or a standalone join if cx omitted)
presets{ button, badge, card, input }null if cva was not passed
utilityClassesRecord<string, string[]>All class names grouped by category
injectStyles() => voidSame function, for manual re-call
createVariants(cva) => presetsAlias 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:

LocationUsed byContents
Chroma-Style/Canonical @chroma/style packagechroma.plugin.json, index.js, tokens.css, utilities.css, variants.js
Chroma/cli/plugins/chroma-style/chroma new, chroma installSynced copy vendored into apps
Chroma/extensions/styling/Chroma Desktop shellCSS + 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.

  • Components & Variants — cva, cx, and props.class resolution
  • 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.

On this page