Styling
Dziry apps are styled with real CSS, resolved at build time. Tailwind v4 is the intended authoring layer, and the build runs the actual Tailwind — your project's own copy, not a reimplementation.
How stylesheets are processed
A stylesheet reaches the compiler by being imported, as in any web project:
// windows/main/index.tsx
import "./app.css";
Multiple imports cascade in the order the module graph evaluates them, so a
sheet imported later wins ties. An inline style={{ … }} beats both, with the
same precedence a browser gives it.
If a stylesheet uses Tailwind, the compiler runs your project's tailwindcss
dependency over it during the build — @import "tailwindcss" resolves against
your node_modules, so your version is the one that runs, and nothing is
written to disk:
/* windows/main/app.css */
@import "tailwindcss";
The compiler then resolves the result against your tree: selectors match, specificity sorts, inheritance applies, shorthands expand, units convert. The output is a style table of integers and floats; the engine never sees a selector.
A utility the compiler cannot handle produces a build warning naming the property — a page that compiles without warnings is a page whose utilities all work.
Static classes
<div className="flex flex-col gap-6 rounded-xl bg-zinc-900 p-6" />;
Static classes resolve to a style id at build time. Nothing further to know.
Conditional classes
Use cn, not string concatenation:
const isBig = signal(false);
const isLight = signal(false);
<div className={cn("box rounded-lg px-4 py-2", { active: isBig })} />;
<div className={cn({ light: isLight })} />;
cn does not return a string. The compiler needs the connection between the
class and the signal that drives it, so it can resolve the class both ways
ahead of time — once a conditional class has become a string, that connection
is gone. Flipping the signal at run time costs a few integer writes into the
style table.
// Wrong: evaluated once at build time against a signal object; never updates.
<div className={"box " + (isBig ? "big" : "")} />
The concatenated form compiles without error and never updates — watch for it.
Inline styles
<div style="color: red; padding: 8px" />;
<div style={{ color: "red", padding: 8, fontWeight: 600 }} />;
A bare number means pixels, except for the genuinely unitless properties —
fontWeight, flexGrow, flexShrink, flex, aspectRatio, gridColumn,
gridRow, zIndex, opacity, lineClamp — the same convention React uses.
Inline styles beat every selector, and both forms cost the runtime nothing.
A non-static inline value is a build error, not a silent drop: once the components are gone there is no node to update. Use a conditional class instead.
State variants
Pseudo-class variants like hover: work without any run-time selector
matching. Each interactive node carries a bitmask of the predicates its
styling reads, plus an offset into a run of precompiled style ids — hovering
costs one 16-bit write.
The supported set is :hover, :active, :focus, :focus-visible,
:checked, :disabled, :open and :invalid. Most are the engine's own
knowledge of pointer and focus; :invalid is driven by your code — a
validate={…} runs, and a rejected field carries the bit until the next
validation clears it.
The cascade is resolved per pseudo-state from scratch rather than as a patch
over the base state, which is what makes per-property hover + focus
merging correct.
The difference shows up in lists. Rows are compiled once and replicated, so
every replica shares one style row — a conditional class on a row's input is
the same class on all of them. A predicate is resolved per node against the
controls table, where each replica has its own entry — so :invalid can be
true for row 3 and false for row 4, and cn("x", { on: sig }) cannot.
CSS coverage
Dziry supports a subset of CSS, scoped to what Tailwind emits. A utility outside the subset produces a build warning naming the property, so an unsupported class is never a silent no-op. (The exact coverage is measured by scripts in the repository — see Contributing.)
Known gaps
The gaps you are most likely to notice:
line-heightis unsupported, sotext-smandtext-lgset the font size and warn about the line height they carry.@media (hover: hover)and@supportsare skipped.@propertyis not: itsinitial-valueis read, which is what makes Tailwind's--tw-*variables resolve.::selectionacceptsbackground-colorandcoloronly — the same short list CSS gives highlight pseudo-elements, since a selection is a range, not a box.box-shadowsupports the ring subset only: no offset, no blur, a solid spread. Everyring-*,inset-ring-*andring-offset-*utility works;shadow-mdwarns and draws nothing.mask-imageandmask-compositeare the largest unimplemented blockers by class count, followed bycalc()over percentages and viewport units.
The user-agent stylesheet
Dziry ships its own user-agent stylesheet rather than Tailwind's preflight —
the preflight is written with selectors Dziry does not implement (:host,
*, ::before, [hidden]) and is optional by design. The template's entry
stylesheet therefore imports theme.css and utilities.css rather than the
umbrella @import "tailwindcss", and restricts scanning with source(none)
plus explicit @source directives so Tailwind only generates utilities your
pages actually use.