Targ Apps Docs
Chroma

Components & Variants

cva and cx — building reusable components with style variants on top of plain e() functions.

Chroma has no compiler and no component abstraction beyond a plain function invoked through e(Component, props, ...children) (see Architecture). That's enough to reuse a component's structure, but reusing its look across variants — a Button that is sometimes primary, sometimes ghost, sometimes sm, sometimes lg — usually means hand-rolling class-string concatenation inside every component. cva and cx exist to give that concatenation the same declarative treatment Chroma gives everything else.

Both are pure class-name utilities: they know nothing about the DOM, signals, or e(). A "reusable component with variants" in Chroma is still just a function — cva only computes the string that goes into its class prop.

cva

Defines a class resolver for a component's variants: cva({ base, variants, compoundVariants, defaultVariants }). Returns a function (props) => string.

import { e, cva } from "./pkg/chroma.js";

const buttonClass = cva({
    base: "btn",
    variants: {
        variant: { primary: "btn-primary", ghost: "btn-ghost" },
        size: { sm: "btn-sm", lg: "btn-lg" },
    },
    compoundVariants: [
        { variant: "primary", size: "lg", class: "btn-primary-lg" },
    ],
    defaultVariants: { variant: "primary", size: "sm" },
});

function Button(props) {
    return e(
        "button",
        { class: buttonClass(props), onClick: props.onClick },
        props.children,
    );
}

e(Button, { variant: "ghost", size: "lg" }, "Cancel"); // class="btn btn-ghost btn-lg"
e(Button, { variant: "primary", size: "lg" }, "OK");   // class="btn btn-primary btn-lg btn-primary-lg"
e(Button, {}, "Save");                                 // class="btn btn-primary btn-sm" (defaults)

Resolution order:

  1. base (optional)string | string[], always included.
  2. For each group in variants (optional, { group: { option: class } }), the option comes from props[group], falling back to defaultVariants[group] when the prop is missing. Unknown options resolve to no class for that group.
  3. compoundVariants (optional)[{ ...selectedOptions, class }]; an entry's class is added only when every named group in it matches the resolved selection.
  4. props.class / props.className, appended verbatim last — callers can still bolt on one-off classes without fighting the resolver.

Variant options aren't limited to strings. If props[group] is a boolean, it's coerced to the string "true"/"false" before matching — so a variant group can branch on a boolean prop directly:

const badgeClass = cva({
    base: "badge",
    variants: {
        disabled: { true: "opacity-50 pointer-events-none", false: "" },
    },
    defaultVariants: { disabled: false },
});

badgeClass({ disabled: true }); // "badge opacity-50 pointer-events-none"

Because the resolver is a plain function, nothing stops you from calling it inside a reactive attribute to vary the class after mount:

function Badge(props) {
    return e("span", { class: () => badgeClass({ ...props, status: status() }) }, props.children);
}

cx

Merges class names, skipping falsy values and flattening arrays — the same shape as the common clsx/classnames helper:

import { cx } from "./pkg/chroma.js";

cx("btn", isActive() && "btn-active", ["a", "b"]); // "btn btn-active a b"
cx("card", null, false, "", "card-highlighted");   // "card card-highlighted"

Reach for cx for one-off conditional classes that don't need cva's named-variant model — e.g. inside a reactive attribute:

e("div", { class: () => cx("card", isActive() && "card-active") });

Building a small design system

Because components stay plain functions, a handful of cva-backed ones compose like any other Chroma component — pass them to e(), nest them, wrap them in Show/For, give them their own hooks:

const badgeClass = cva({
    base: "badge",
    variants: { tone: { info: "badge-info", danger: "badge-danger" } },
    defaultVariants: { tone: "info" },
});

function Badge(props) {
    return e("span", { class: badgeClass(props) }, props.children);
}

function StatusList({ items }) {
    return e(For, {
        each: items,
        children: (item) => e(Badge, { tone: item.tone }, item.label),
    });
}

See API Reference for the full signatures, Control Flow for Show/For, and example/index.html in the repository for a runnable Button built this way.

On this page