Targ Apps Docs
Chroma

Examples

Two runnable Chroma apps — a full hooks demo and a larger real-world example.

Both examples live under example/ in the repository and run directly against pkg/chroma.js produced by ./build.sh — no bundler, no build step.

Hooks demo (example/index.html)

A single-file tour of every hook: hookState, useEffect, useLayoutEffect, useMemo, useCallback, useRef, useReducer, createContext/useContext, and useDebugValue, plus a Button component built with cva to show the reusable-component-with-variants pattern (see Components & Variants). The file also defines a few extra custom hooks purely to demonstrate composition — useScopedState (a thin hookState wrapper), useSelector (a useMemo wrapper that derives a value from a getter), and useMultipleState (bundles several hookState pairs into one { value, setValue }-style object) — none of which add new primitives, they're just hookState/useMemo composed differently.

import {
    hookState, render, e, Fragment,
    useEffect, useLayoutEffect, useMemo, useCallback,
    useRef, useReducer, createContext, useContext, useDebugValue,
    cva,
} from "../pkg/chroma.js";

// Reusable component with variants: same Button, different look per props —
// cva resolves the class; the component stays a plain function built with e().
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,
    );
}

// Context: shares [theme, setTheme] with any descendant.
const Theme = createContext(null);

// Custom hook composing hookState + useMemo + useCallback.
function useCounter(initial) {
    const [count, setCount] = hookState(initial);
    const doubled = useMemo(() => count() * 2, [count]);
    const inc = useCallback(() => setCount(count() + 1));
    useDebugValue("useCounter");
    return { count, doubled, inc };
}

function Counter() {
    const { count, doubled, inc } = useCounter(0);

    useEffect(() => {
        document.title = `Chroma — count ${count()}`;
        return () => console.log("cleanup: count was", count());
    }, [count]);

    return e(
        "section", null,
        e("h2", null, "hookState + useMemo + useEffect"),
        e("button", { onClick: inc }, "Count: ", count),
        e("p", null, "Doubled (useMemo): ", doubled),
    );
}

function Reducer() {
    const [total, dispatch] = useReducer((state, action) => {
        switch (action) {
            case "add": return state + 1;
            case "sub": return state - 1;
            case "reset": return 0;
            default: return state;
        }
    }, 0);

    return e(
        "section", null,
        e("h2", null, "useReducer"),
        e("button", { onClick: () => dispatch("sub") }, "−"),
        e("strong", null, " ", total, " "),
        e("button", { onClick: () => dispatch("add") }, "+"),
        e("button", { onClick: () => dispatch("reset") }, "reset"),
    );
}

function Focusable() {
    const inputRef = useRef(null);

    // Synchronous, before paint: the node is already in inputRef.current.
    useLayoutEffect(() => {
        inputRef.current.placeholder = "set by useLayoutEffect";
    }, []);

    return e(
        "section", null,
        e("h2", null, "useRef + useLayoutEffect"),
        e("input", { ref: inputRef }),
        e("button", { onClick: () => inputRef.current.focus() }, "Focus"),
    );
}

function ThemeButton() {
    const [theme, setTheme] = useContext(Theme);
    return e(
        "button",
        { onClick: () => setTheme(theme() === "light" ? "dark" : "light") },
        "Toggle theme (current: ", () => theme(), ")",
    );
}

function Buttons() {
    return e(
        "section", null,
        e("h2", null, "cva — reusable component with variants"),
        e(Button, { variant: "primary", size: "sm" }, "Save"),
        e(Button, { variant: "ghost", size: "sm" }, "Cancel"),
        e(Button, { variant: "primary", size: "lg" }, "Confirm"),
    );
}

function App() {
    const [theme, setTheme] = hookState("light");

    // Provider children go as a function (lazy) so useContext
    // sees them with the provider already active.
    return e(Theme.Provider, { value: [theme, setTheme] }, () =>
        e(
            "main",
            { class: theme }, // reactive attribute: a getter as the value
            e("h1", null, "Chroma — hooks"),
            e(ThemeButton),
            e(Counter),
            e(Reducer),
            e(Focusable),
            e(Buttons),
        ),
    );
}

render(App, document.getElementById("app"));

Run it:

./build.sh
python3 -m http.server 8000
# open http://localhost:8000/example/index.html

Real-world app (example/targapps_kvdb.html)

A larger, single-file CRM-style app (~1,180 lines) that exercises the full surface together: hookState, useEffect, Fragment, and the two control-flow components, Show and For.

import { hookState, render, e, Fragment, Show, For, useEffect } from "../pkg/chroma.js";

It models a login screen plus a dashboard with clients and projects stored in local component state (persisted to localStorage between reloads), and demonstrates:

  • Show for gating the dashboard behind a login screen (when: loggedIn, fallback: () => e(LoginScreen)).
  • For for rendering the clients and projects tables from arrays kept in hookState, rebuilding rows whenever a new array reference is written (see Control Flow for why reference identity matters).
  • useEffect for side effects like syncing document.title and driving a confirmation toast.
  • A modal-driven create/edit flow for clients and projects, with form state tracked in a single hookState object and updated via the updater form (setFormData(f => ({ ...f, ...updates }))).
  • Plain data-driven UI: no routing library, no state-management library — just signals and functions.

Use this file as a reference for structuring a bigger app: keep data in hookState/useReducer at the top of App, derive views with useMemo, and delegate branching/lists to Show/For instead of hand-rolled conditionals over DOM nodes.

On this page