Styling
Four styling tools — all functions, no build step required.
sheet() — Scoped CSS Classes
Section titled “sheet() — Scoped CSS Classes”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.
styles() — Reactive Inline Styles
Section titled “styles() — Reactive Inline Styles”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")cx() — Static Class Composition
Section titled “cx() — Static Class Composition”import { cx, div } from "@whisq/core";
div({ class: cx("btn", isPrimary && "btn-primary", isLarge && "btn-lg") })div({ class: cx("card", { active: true, disabled: false }) })rcx() — Reactive Class Composition
Section titled “rcx() — Reactive Class Composition”import { signal, rcx, div } from "@whisq/core";
const variant = signal("primary");
div({ class: rcx( "btn", () => variant.value === "primary" && "btn-primary", () => loading.value && "btn-loading", ),})theme() — Design Tokens
Section titled “theme() — Design Tokens”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 insrc/styles.ts. Importstyles.tstransitively fromApp.tsso the call runs on first import. The tokens become CSS custom properties on:rootand 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 ownclassMapand 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-memoryclassMapso 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 returnedclassMapinto a<style>block in your SSR template.
See /api/theme/ and /api/sheet/ for the full per-API reference.
Using External CSS Frameworks
Section titled “Using External CSS Frameworks”Whisq renders real DOM elements — div() produces an actual <div>, button() produces a <button>. Any CSS framework that works with HTML works with Whisq.
Tailwind CSS
Section titled “Tailwind CSS”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}`);Plain CSS / CSS Modules
Section titled “Plain CSS / CSS Modules”Import a CSS file in your entry point and use class names directly:
import "./styles.css";import { div, p } from "@whisq/core";
div({ class: "card" }, p({ class: "card-title" }, "Hello"),);When to Use What
Section titled “When to Use What”| Approach | Best for |
|---|---|
sheet() | Component-scoped styles, no external deps |
styles() | Reactive inline styles |
| Tailwind / UnoCSS | Utility-first, rapid prototyping |
| Plain CSS | Existing stylesheets, CSS variables |
You can mix approaches — use sheet() for component logic and Tailwind for layout utilities in the same project.
Next Steps
Section titled “Next Steps”- Conditional Rendering — Show/hide UI reactively
Docs current to v0.1.0-alpha.9 . All releases →