Components
How components, props, and children work in Chroma, plus cva and cx for reusable styled components.
A Chroma component is a plain function. e(Component, props, ...children) calls it exactly once, passes it a props object, and mounts the DOM node it returns. There is no compiler and no re-render.
The component model
// src/components/Card.chr
import { e } from "../../pkg/chroma.js";
export function Card(props) {
return e("section", { class: "card" },
e("h2", null, props.title),
props.children,
);
}
// usage
e(Card, { title: "Profile" }, e("p", null, "Hello"));Runs once
The function body executes a single time. Hooks inside it allocate signals once, and nothing re-runs the body later. Only the function children and function props you return stay live. See Reactivity.
Props
e copies your props into a new object and calls the component with it. Props are not reactive by themselves. To keep a value live across the component boundary, pass a getter and call it where it is used:
function Label(props) {
return e("span", null, props.text); // props.text is a getter: reactive
}
e(Label, { text: () => count() }); // live
e(Label, { text: count() }); // frozen at mountChildren
Children after the props are collected into props.children:
- One child:
props.childrenis that value. - Several children:
props.childrenis an array. - No children:
props.childrenis not set.
Place props.children inside your returned tree; arrays are flattened and null, undefined, and booleans render nothing. A child is evaluated before the component runs, so e(Card, null, e(Expensive)) builds Expensive first. When a component must control when children run (as Show and Provider do), pass the child as a function and call it yourself:
e(Show, { when: loggedIn }, () => e(Dashboard));Control-flow components also accept children as a prop: e(For, { each, children }) is the same as e(For, { each }, children). See Control flow.
Fragments
Return several siblings without a wrapper element with e(Fragment, null, a, b).
cva
Defines a class resolver for a component's variants: cva({ base, variants, compoundVariants, defaultVariants }) returns (props) => string. It only computes a class string; it knows nothing about the DOM or signals.
// src/components/Button.chr
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" },
});
export 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, {}, "Save"); // class="btn btn-primary btn-sm"Resolution order:
base(optional,string | string[]) is always included.- For each group in
variants, the option comes fromprops[group], falling back todefaultVariants[group]. Unknown options add no class. compoundVariantsentries add theirclassonly when every named group matches the resolved selection.props.classorprops.classNameis appended last.
Boolean props are coerced to "true" / "false" before matching:
const badgeClass = cva({
base: "badge",
variants: { disabled: { true: "opacity-50", false: "" } },
defaultVariants: { disabled: false },
});
badgeClass({ disabled: true }); // "badge opacity-50"To vary classes after mount, call the resolver inside a reactive attribute:
// src/components/StatusBadge.chr
import { e, cva } from "../../pkg/chroma.js";
const statusClass = cva({
base: "badge",
variants: { status: { ok: "badge-ok", error: "badge-error" } },
defaultVariants: { status: "ok" },
});
export function StatusBadge(props) {
// props.status is a getter returning "ok" or "error"
return e("span", { class: () => statusClass({ status: props.status() }) }, props.children);
}cx
Merges class names, skipping falsy values and flattening arrays:
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"Use it for one-off conditional classes, usually inside a reactive attribute:
e("div", { class: () => cx("card", isActive() && "card-active") });Composing components
// src/components/StatusList.chr
import { e, For, cva } from "../../pkg/chroma.js";
const toneClass = cva({
base: "badge",
variants: { tone: { info: "badge-info", danger: "badge-danger" } },
defaultVariants: { tone: "info" },
});
function Badge(props) {
return e("span", { class: toneClass(props) }, props.children);
}
export function StatusList(props) {
return e(For, {
each: () => props.items(),
children: (item) => e(Badge, { tone: item.tone }, item.label),
});
}class and className are both accepted on elements. Signatures: API reference. Hooks inside components: Hooks.