Skip to content

Four styling tools — all functions, no build step required.

import { sheet, div, h2, p } from "@whisq/core";
const s = sheet({
card: {
padding: "1.5rem",
borderRadius: "12px",
background: "#fff",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
"&:hover": { background: "#f5f5f5" },
},
title: { fontSize: "1.25rem", fontWeight: 600 },
});
div({ class: s.card },
h2({ class: s.title }, "Hello"),
p("World"),
)

Classes are auto-scoped (e.g., wq0_card). A <style> tag is injected automatically.

Nested selectors beyond &:hover — pseudo-elements (&::before), compound selectors (&:checked::after), combinators (& > span), attribute selectors (&[open]), @media / @supports / @container at-rules — all work. See /api/sheet/#supported-nested-selectors for the full reference with one example per category and documented limitations.

import { signal, styles, div } from "@whisq/core";
const dark = signal(false);
div({
style: styles({
padding: "1rem",
background: () => dark.value ? "#111" : "#fff",
color: () => dark.value ? "#fff" : "#111",
}),
}, "Content")
import { cx, div } from "@whisq/core";
div({ class: cx("btn", isPrimary && "btn-primary", isLarge && "btn-lg") })
div({ class: cx("card", { active: true, disabled: false }) })
import { signal, rcx, div } from "@whisq/core";
const variant = signal("primary");
div({
class: rcx(
"btn",
() => variant.value === "primary" && "btn-primary",
() => loading.value && "btn-loading",
),
})
import { theme, sheet } from "@whisq/core";
theme({
color: { primary: "#4386FB", text: "#111827", bg: "#ffffff" },
space: { sm: "0.5rem", md: "1rem", lg: "1.5rem" },
radius: { md: "8px", lg: "12px" },
});
// Use in sheet():
const s = sheet({
card: {
background: "var(--color-bg)",
padding: "var(--space-lg)",
borderRadius: "var(--radius-lg)",
},
});

Lifecycle: where to call theme() and sheet()

Section titled “Lifecycle: where to call theme() and sheet()”
  • Call theme() once, at module scope in src/styles.ts. Import styles.ts transitively from App.ts so the call runs on first import. The tokens become CSS custom properties on :root and stay available everywhere.

  • Duplicate theme() calls = last-call-wins. A second call replaces the first <style> block entirely. This is intentional — it enables theme-switching (theme(lightTokens)theme(darkTokens) in a toggle handler) without leaking old tokens. Since alpha.9, dev mode warns on duplicate calls to catch accidental double-imports; pass { silent: true } on intentional theme-switch calls. See /api/theme/#duplicate-call-warning-since-alpha9.

  • sheet() can be called per-module wherever you want scoped styles. Each call returns its own classMap and injects its own <style> block; calling at module scope (e.g. const s = sheet({...}) at the top of a component file) keeps the call once-per-module.

  • SSR-safe (since alpha.8). On the server (typeof document === "undefined"):

    • theme() is a no-op — no <style> tag is written; client hydration applies the theme on mount.
    • sheet() returns the in-memory classMap so server-rendered HTML can reference correct class names — but skips the DOM injection step. Hydration injects on mount.

    Pre-alpha.8 both threw ReferenceError: document is not defined. If your SSR pipeline needs styles inlined into server HTML for first-paint, emit them yourself by walking the returned classMap into a <style> block in your SSR template.

See /api/theme/ and /api/sheet/ for the full per-API reference.

Whisq renders real DOM elements — div() produces an actual <div>, button() produces a <button>. Any CSS framework that works with HTML works with Whisq.

import { signal, div, button, span } from "@whisq/core";
const count = signal(0);
div({ class: "flex items-center gap-4 p-8 bg-gray-900 rounded-xl" },
button({
class: "w-10 h-10 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-bold",
onclick: () => count.value--,
}, "-"),
span({ class: "text-3xl font-mono text-cyan-400" }, () => `${count.value}`),
button({
class: "w-10 h-10 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-bold",
onclick: () => count.value++,
}, "+"),
);

Reactive classes work naturally:

div({
class: () => count.value > 0
? "text-green-400 font-bold"
: "text-red-400 font-bold",
}, () => `${count.value}`);

Import a CSS file in your entry point and use class names directly:

main.ts
import "./styles.css";
import { div, p } from "@whisq/core";
div({ class: "card" },
p({ class: "card-title" }, "Hello"),
);
ApproachBest for
sheet()Component-scoped styles, no external deps
styles()Reactive inline styles
Tailwind / UnoCSSUtility-first, rapid prototyping
Plain CSSExisting stylesheets, CSS variables

You can mix approaches — use sheet() for component logic and Tailwind for layout utilities in the same project.

Docs current to v0.1.0-alpha.9 . All releases →