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.htmlReal-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:
Showfor gating the dashboard behind a login screen (when: loggedIn,fallback: () => e(LoginScreen)).Forfor rendering the clients and projects tables from arrays kept inhookState, rebuilding rows whenever a new array reference is written (see Control Flow for why reference identity matters).useEffectfor side effects like syncingdocument.titleand driving a confirmation toast.- A modal-driven create/edit flow for clients and projects, with form state tracked in a single
hookStateobject 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.