Zazz 0.5 alpha
Zazz 0.5 replaces utility classes with style utilities: inline custom properties that keep the cascade and cascade layers intact.
Zazz 0.5 alpha is a huge milestone that redefines the future of the Zazz design system and primitives library.
Introduction of style utilities
This release removes the Tailwind-like class utility system in favor of an inline CSS custom property syntax we call style utilities. This rework opens up huge wins for performance, file size, and flexibility. It also sets Zazz up to take full advantage of the new if(), style(), and @function CSS features as soon as they're widely available, which will greatly reduce file size and add a huge number of style utilities that would otherwise bloat the system.
0.4.x syntax
<section class="py-xl text-center">
<div class="container">
<hgroup class="flex flex-col gap-sm">
<span class="text-eyebrow">Utilities</span>
<h1 class="text-h1" tabindex="-1">Responsive utilities</h1>
<p class="text-lg text-muted-foreground @md:max-w-screen-sm">
Every utility below carries an <code>@</code> breakpoint prefix. They are
mobile-first, gated on the global breakpoint flags from the body container, so they
react to the viewport. Resize to watch them move.
</p>
<a href="#" class="ui-button mt-md" data-variant="primary">Get started</a>
</hgroup>
</div>
</section>0.5.0 syntax
<section
style="
--py: 24;
--text-align: center;
"
>
<div data-ui="layout">
<hgroup
style="
--display: flex-col;
--gap: 6;
--text-align: center;
"
>
<span data-ui="text-eyebrow">Utilities</span>
<h1 data-ui="text-h1" tabindex="-1">Responsive utilities</h1>
<p
data-ui="text-lg"
style="
--max-w: 100%;
--max-w--md: 60ch;
--text-wrap: balance;
--text: var(--color-muted-foreground);
"
>
Every utility below carries an <code>@</code> breakpoint prefix. They are
mobile-first, gated on the global breakpoint flags from the body container, so they
react to the viewport. Resize to watch them move.
</p>
<a
href="#"
data-ui="button"
data-button-variant="primary"
style="--mt: 24"
>Get started</a>
</hgroup>
</div>
</section>Why not normal inline styles?
You have likely never seen a syntax quite like this, and your first question might be, "why not use normal inline styles?"
The essential idea is that inline styles break the CSS cascade: they win over every selector, and they ignore cascade layers. If you've ever had to deal with inline styles that you're unable to remove from the markup, you know how much suffering they can cause.
Style utilities, on the other hand, preserve the cascade and correctly use Zazz's cascade layers by using the style attribute and the variable name inside it as the selector itself.
The simplest version
The simplest expression of this idea looks like this:
@property --p { syntax: "*"; inherits: false; }
@layer ui.utilities {
:where([style*="--p:"]) {
padding: var(--p);
}
}The trick is to keep style utilities from inheriting values from parent elements (that's the inherits: false in the @property registration). Once you do that, each element's styles become scoped to itself. The result is a syntax that feels and reads more like StyleX but uses familiar, Tailwind-like shorthand naming conventions that many developers are already used to.
Design system values and arbitrary values together
Zazz style utilities take this idea even further by allowing design system values and arbitrary values to live together. All the following values are valid:
<div
style="
--p: 24; /* Spacing scale: equivalent to `calc(24 * var(--spacing, 0.25rem))` */
--p: 6rem; /* Fixed value: can be any regular length value */
--p: var(--space-md); /* Variable: can be any CSS variable of the same value type */
"
></div>Zazz uses CSS property types and type invalidation to switch between arbitrary fixed values and spacing scale values without functions or if statements.
The real utility
The real utility looks closer to this:
@property --p { syntax: "*"; inherits: false; }
@property --_p { syntax: "*"; inherits: false; }
@property --_p-resolved { syntax: "*"; inherits: false; }
@property --_p-len { syntax: "<length-percentage>"; inherits: false; initial-value: 0px; }
@property --_p-num { syntax: "<number>"; inherits: false; initial-value: 0; }
@layer ui.utilities {
:where([style*="--p:"], [style*="--p--"]) {
--_p-resolved: var(--_p-2xl, var(--_p-xl, var(--_p-lg, var(--_p-md, var(--_p-sm, var(--_p))))));
}
:where([style*="--p:"]) {
--_p: var(--p);
--_p-len: var(--_p-resolved);
--_p-num: var(--_p-resolved);
padding: calc(var(--_p-len) + var(--_p-num) * var(--spacing));
}
}The --_p-sm through --_p-2xl values come from breakpoint tiers like --p--md: 12. Much of this code will be simplified once if() CSS conditionals become widely available, but this solution brings type-driven conditional values to Zazz today.
How this affects legacy system migration
There are two core target audiences for Zazz UI. First, Zazz is a no-build, drop-in solution that any technology can use without adopting new infrastructure. But Zazz is also built specifically for legacy applications, which drives the core decision to stay out of the class attribute.
Many legacy applications are stuck with decades of fragile technical debt and rely heavily on classes as their primary target for styles and JavaScript selectors. On top of that, their backend language of choice often manipulates classes or depends on specific classes in ways that are difficult to track down or too fragile to just delete.
Zazz stays out of your classes and can live beside them without worrying about name conflicts or unforeseen downstream bugs. Style utilities themselves never conflict with other JavaScript libraries that use the inline style attribute for things like animations or state changes. When JavaScript is turned on, your style utilities can be automatically preserved, even if a library tries to clear the style attribute completely, with the style guard. The migration guide covers bringing Zazz into an existing app.
Other notable features
data-uiidentity. Zazz never reads or writes a class. Components aredata-ui="button", presets aredata-button-variant="primary", and parts aredata-dialog-slot="header".- Breakpoints, states, and groups are suffixes on the same utility:
--w--md,--bg--hover,--group-opacity--hover. Plus the newstartingstate for@starting-styleand an experimentalstuckstate. - Container breakpoints. Five Tailwind names (
smthrough2xl) with thresholds inch, read from the nearest container instead of the window. - Layout primitive replaces
.containerwith a band grid. - Pseudo-element utilities (
--before-*,--after-*) for accent bars and badge dots without a stylesheet. - New utilities: border shorthands (
--border-b: 1), gradients, Tailwind-style grid placement, and thedivide-x/divide-yswitches. - Color roles are
--color-*tokens built onlight-dark(), anddata-ui-themepins a theme. See theming. - A new card primitive, and button and badge now cover all four status colors: info, success, warning, and destructive.
- Opt-in in-page navigation that keeps marked elements (a toast stack, a playing video) alive across pages.
<ui-debug>audits your markup in the browser. The same audit runs in the VS Code extension and the ESLint plugin.- The
zazz-uiCLI copies primitives into your project and merges later updates with your edits. See installation. - AI-ready docs:
AUTHORING.mdships in the package, and the site publishesllms.txt. - A playground for trying markup in the browser.
The changelog lists every breaking change with a one-line migration.