Targ Apps Docs
Chroma

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 mount

Children

Children after the props are collected into props.children:

  • One child: props.children is that value.
  • Several children: props.children is an array.
  • No children: props.children is 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:

  1. base (optional, string | string[]) is always included.
  2. For each group in variants, the option comes from props[group], falling back to defaultVariants[group]. Unknown options add no class.
  3. compoundVariants entries add their class only when every named group matches the resolved selection.
  4. props.class or props.className is 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.

On this page