sheet()
Generate scoped class names from JavaScript objects. Auto-injects a <style> tag.
Signature
Section titled “Signature”function sheet<T extends SheetDef>(definitions: T): SheetResult<T>Parameters
Section titled “Parameters”| Param | Type | Description |
|---|---|---|
definitions | T | Object mapping class names to CSS rules |
Returns
Section titled “Returns”Object where each key is a scoped class name string. Also includes a .cls() helper.
Examples
Section titled “Examples”import { sheet, div, h2, p } from "@whisq/core";
const s = sheet({ card: { padding: "1.5rem", borderRadius: "12px", background: "#fff", "&:hover": { background: "#f5f5f5" }, }, title: { fontSize: "1.25rem", fontWeight: 600 },});
div({ class: s.card }, h2({ class: s.title }, "Hello"), p("World"))Supported nested selectors
Section titled “Supported nested selectors”Two buckets, resolved by the leading character of the key:
&-prefixed keys — the leading&is dropped and the rest of the key is appended verbatim to the scoped class name. So"&:hover"becomes.wq0_card:hover,"& > span"becomes.wq0_card > span, and so on. Anything valid as a CSS suffix works — pseudo-classes, pseudo-elements, combinators, attribute selectors,:not(),:has(), etc.@-prefixed keys — wrapped around the class rule."@media (min-width: 768px)"becomes@media (min-width: 768px){.wq0_card{...}}.@supports,@container, and any other conditional at-rule work the same way.
Pseudo-classes
Section titled “Pseudo-classes”sheet({ btn: { background: "#333", color: "#fff", "&:hover": { background: "#555" }, "&:focus-visible": { outline: "2px solid var(--color-accent)" }, "&:disabled": { opacity: 0.5, cursor: "not-allowed" }, "&:active": { background: "#222" }, },})Pseudo-elements
Section titled “Pseudo-elements”sheet({ card: { position: "relative", "&::before": { content: '""', position: "absolute", inset: 0 }, "&::after": { content: '"★"', position: "absolute", top: "0.5rem", right: "0.5rem" }, }, field: { padding: "0.5rem", "&::placeholder": { color: "var(--color-text-muted)" }, },})Compound selectors
Section titled “Compound selectors”Stack pseudo-classes and pseudo-elements directly — the whole suffix is concatenated.
sheet({ toggle: { "&:checked::after": { content: '"✓"', color: "var(--color-success)" }, "&:hover:not(:disabled)": { background: "#555" }, "&:has(:focus-visible)": { outline: "2px solid var(--color-accent)" }, },})Combinators (descendants, children, siblings)
Section titled “Combinators (descendants, children, siblings)”sheet({ list: { listStyle: "none", padding: 0, "& > li": { padding: "0.5rem", borderBottom: "1px solid #eee" }, "& a": { color: "var(--color-accent)", textDecoration: "none" }, "& a:hover": { textDecoration: "underline" }, "& > li + li": { marginTop: "0.25rem" }, },})Attribute selectors and class modifiers
Section titled “Attribute selectors and class modifiers”sheet({ dialog: { display: "none", "&[open]": { display: "block" }, '&[data-state="entering"]': { animation: "fade-in 150ms" }, }, btn: { background: "#333", "&.primary": { background: "var(--color-primary)" }, "&.danger": { background: "var(--color-danger)" }, "&.small": { padding: "0.25rem 0.5rem" }, },})At-rules (media, supports, container)
Section titled “At-rules (media, supports, container)”sheet({ card: { padding: "1rem", display: "block", "@media (min-width: 768px)": { padding: "2rem" }, "@supports (display: grid)": { display: "grid", gap: "1rem" }, "@container (max-width: 400px)": { fontSize: "0.875rem" }, },})SSR behaviour (since alpha.8)
Section titled “SSR behaviour (since alpha.8)”On the server (typeof document === "undefined"), sheet() returns the in-memory classMap as usual — server-rendered HTML can reference the correct class names for the client to hydrate against — but skips the DOM injection step. The styles aren’t written to a <style> tag on the server; client-side hydration injects them on mount.
Previously threw ReferenceError: document is not defined at the injection line. The fix in alpha.8 is the same internal injectCSS() short-circuit that theme() and persistedSignal() already use.
If your SSR pipeline needs the styles inlined into server HTML (e.g. for first-paint correctness without a hydration flash), emit them yourself by walking the returned classMap and serializing the rules into a <style> block in your SSR template.
Limitations
Section titled “Limitations”- Single level of nesting. Nested rules are flat
{ property: value }objects — you cannot nest a&- or@- key inside another nested block. To express “the hover state of a descendant,” combine the selectors yourself:"&:hover > span"rather than"&:hover": { "& > span": { ... } }. - No global / top-level selectors.
sheet()only emits scoped class rules. For global resets or tag-level styling, inject a<style>tag manually or use a separate CSS file. &must be the first character of the key. The engine only processes keys that start with&or@. A key like".parent","> span", or"span &"is silently mishandled — it goes into the base rule and is serialized as a CSS property, producing garbage output without any error. This is the footgun to watch for: always start your nested keys with&or@.- No SCSS-style
&substitution mid-selector. The engine does not scan for&beyond the leading position. Writing"&:hover & > span"emits.cls:hover & > span— the second&stays literal and is invalid CSS.
Docs current to v0.1.0-alpha.9 . All releases →