Capped + centered
Full-bleed background, capped/centered content.
The spacing scale and layout primitives every page should build from — instead of a new one-off flex flex-col gap-3.5 px-4.5 every time. zeyo-ui/layout/*, plus a padding prop on Card.
zeyo-ui/spacing.css — plain CSS custom properties (deliberately not a Tailwind @theme key — see the file's own comment for why). xs-xl cover component-internal gaps/padding; 2xl-4xl are the section-band tier, for a full-bleed page section's own vertical padding (Section's `padding` prop below), a range component gaps never need to reach. Reach for these before a new arbitrary value; layout-only Tailwind spacing is still fine where none of these fit.
--spacing-xs: calc(var(--spacing) * 2); /* 8px */
--spacing-sm: calc(var(--spacing) * 3); /* 12px */
--spacing-md: calc(var(--spacing) * 5); /* 20px */
--spacing-lg: calc(var(--spacing) * 8); /* 32px */
--spacing-xl: calc(var(--spacing) * 12); /* 48px */
--spacing-2xl: calc(var(--spacing) * 16); /* 64px */
--spacing-3xl: calc(var(--spacing) * 24); /* 96px */
--spacing-4xl: calc(var(--spacing) * 32); /* 128px */The page max-width wrapper every top-level route renders inside — variant='full' skips the cap entirely for edge-to-edge shells, and variant='fill' is a padded column exactly the height of the viewport under the TopBar, for screens like the Unibox whose panes scroll inside themselves.
<Shell variant="wide"> <!-- max-width: 1200px -->
...
</Shell>
<Shell variant="narrow"> <!-- max-width: 720px -->
...
</Shell>
<Shell variant="fill"> <!-- viewport height minus the TopBar -->
...
</Shell>A vertical block with a semantic bottom margin — the space between one section of a page and the next.
<Section spacing="sm">...</Section>
<Section spacing="md">...</Section>
<Section spacing="lg">...</Section>padding + bg turn Section into a full-bleed page band — the colored, padded section every landing page reimplements slightly differently (background: X; padding: Npx var(--spacing-xl);) — instead of a new hand-written CSS rule per section. padding sets top/bottom via the section-band tier (2xl-4xl reach here, xs-xl don't); left/right is always var(--spacing-xl), dropping to var(--spacing-md) under 700px — that half doesn't vary section to section, only the vertical number does. Omit padding for the plain spacer-only behavior above.
bg="tertiary" padding="2xl"
<Section bg="tertiary" padding="4xl">
<div class="page-inner">...</div>
</Section>contentWidth caps + centers children in an inner max-width/margin:0 auto wrapper — the div every full-bleed section band needs when its content shouldn't stretch the full width. centered adds text-align: center on the section itself, cascading down. Both replace a page-local max-width/margin/text-align class on the section's own wrapper div.
Full-bleed background, capped/centered content.
<Section bg="sf" padding="lg" contentWidth="360px" centered>
<Title level={5}>Capped + centered</Title>
<Text color="secondary" size="sm">Full-bleed background, capped/centered content.</Text>
</Section>ref is a bindable DOM ref — bind:ref gets the real section element, same as bind:this on a plain element. This is what unblocks a Section that also needs an IntersectionObserver/scroll target, without falling back to a hand-written section.
bind:ref={section} points at this element
let section: HTMLElement | null = $state(null);
</script>
<Section bind:ref={section} bg="tertiary" padding="lg">...</Section>
<button onclick={() => section?.scrollIntoView({ behavior: 'smooth' })}>Scroll to it</button>Vertical flex list with a scale-backed gap — for any stacked column of rows/cards. gap='none' is for a dense list whose rows are meant to sit flush, relying on each row's own border-bottom for separation instead of a visible gap between them.
<Stack gap="sm">
<div>Row one</div>
<div>Row two</div>
<div>Row three</div>
</Stack>
<!-- gap="none" — flush rows, separated only by their own border -->
<Stack gap="none">
<div class="list-row">Row one</div>
<div class="list-row">Row two</div>
</Stack>Horizontal, wrapping flex row with a scale-backed gap — the filter/toolbar row above a table (resize the window to see it wrap). nowrap keeps the items on one line instead, for a row that truncates one of them, such as a list row's header.
<Cluster gap="sm">
<Badge color="info">Active</Badge>
<Badge color="warning">Paused</Badge>
<Badge color="success">Completed</Badge>
</Cluster>
<Cluster gap="xs" nowrap><!-- one line: a truncating item + badges --></Cluster>One component for fixed-column stat grids AND two-column splits. minColumnWidth uses auto-fit/minmax so the column count adapts continuously to the available width — no breakpoint to hand-pick. stack collapses a fixed columns split to 1fr below 960px — Grid sets its columns via a style: directive (an inline style), which no external stylesheet can override without !important, so that override lives inside Grid itself instead of every caller hand-rolling a page-local :global(.grid-layout) media query with !important. fill is for a grid inside a flex column (like a Shell variant='fill'): it takes the space left and lets the rows shrink to it, so cards that hold their own scrolling list scroll inside the grid instead of stretching it.
columns={4}
columns="2fr 1fr"
minColumnWidth="120px" — resize the window
columns="1fr 300px" stack — shrink the browser below 960px
<Grid columns={4} gap="md">
<Stat>...</Stat>
<Stat>...</Stat>
<Stat>...</Stat>
<Stat>...</Stat>
</Grid>
<Grid columns="2fr 1fr" gap="md"><!-- two-column split --></Grid>
<Grid minColumnWidth="140px" gap="sm"><!-- responsive, no breakpoint to pick --></Grid>
<Grid columns="1fr 300px" gap="md" stack><!-- collapses to 1 column below 960px --></Grid>
<Grid columns="1fr 2fr" fill><!-- fills a flex column; cards inside scroll, not stretch --></Grid>Sidebar+content app shell — one shared collapse point (1024px) instead of every page picking its own breakpoint. Shrink the window below that to see it stack.
<SplitShell sidebarWidth={200}>
{#snippet sidebar()}...{/snippet}
{#snippet content()}...{/snippet}
</SplitShell>Card's own padding prop — reach for this before a hand-picked p-5 or an arbitrary py-[22px] px-6.
<Card padding="sm">...</Card>
<Card padding="md">...</Card>
<Card padding="lg">...</Card>class is layout-only, same rule as Button/Text — margin, or (as here) a conditional class computed by the caller for state the primitive itself has no prop for. Cluster/Stack/Grid/Section always render their own base class too (cluster/stack/grid-layout/nothing) — yours is appended, not a replacement. This is the escape hatch for a highlighted/active row in a list, without going back to a hand-written flex div.
<Cluster class={active === i ? 'row-active' : ''} gap="sm">
Row {i}
</Cluster>