Layout and craft
Small rules that keep Zenox interfaces consistent, each with an ID to cite in reviews and prompts.
These rules cover the details that separate a consistent interface from one that only uses the
right components. Each has an ID: cite it in a review comment or an agent prompt
(zx/heading-case) instead of explaining it again.
Rules
zx/body-size Running text is text-sm
Body copy, buttons, form controls, table cells, help text and descriptions are text-sm
(12.5px). Larger sizes are for titles and figures only. text-xs (11px, the floor) is for
captions, badges and axis labels: never a sentence, a description or a code block, and never the
only place a user reads something they must act on.
// Good
<p className="text-sm text-text-secondary">Scans run every night at 02:00.</p>
// Avoid
<p className="text-base">Scans run every night at 02:00.</p>
<p className="text-xs text-text-muted">Scans run every night at 02:00.</p>
zx/heading-case Sentence case for every title
Capitalize only the first word and proper nouns. Never set a title in capitals; the caption style is for short group labels only. See Writing.
// Good
<SectionHeading title="Notification channels" />
// Avoid
<SectionHeading title="Notification Channels" />
<SectionHeading title="NOTIFICATION CHANNELS" />
zx/font-weight Weight follows the title’s role
Page and section headings are font-semibold; container titles (Card, Dialog, Sheet, Drawer) are
font-medium, so they stay below the section heading above them; compact 14px titles are
font-semibold, or they would not stand apart from the medium labels around them. Labels,
buttons and emphasis inside text are font-medium. Nothing is font-bold: even KPI figures in a
Stat Tile are semibold. See Typography.
// Good
<h2 className="text-lg font-semibold">Exposure</h2>
<CardTitle>Microsoft 365 connector</CardTitle>
<p>Credentials for <span className="font-medium">acme.com</span> were found.</p>
// Avoid: a container title as heavy as the section heading above it
<CardTitle className="font-semibold">Microsoft 365 connector</CardTitle>
<h2 className="text-lg font-bold">Exposure</h2>
zx/tracking Leave letter spacing alone
Do not change tracking-* on running text or titles. The only tracked text is the uppercase
caption, which uses tracking-wider or tracking-widest from the theme.
zx/related-spacing Related text sits closer together
A title and its description are closer to each other than to the content that follows them.
// Good
<div className="grid gap-4">
<div className="grid gap-1">
<h3 className="text-lg font-semibold">Detection rules</h3>
<p className="text-sm text-text-secondary">Rules decide which events become alerts.</p>
</div>
<Button variant="outline">Create rule</Button>
</div>
// Avoid: the description is as far from its title as from the button
<div className="grid gap-4">
<h3 className="text-lg font-semibold">Detection rules</h3>
<p className="text-sm text-text-secondary">Rules decide which events become alerts.</p>
<Button variant="outline">Create rule</Button>
</div>
zx/optical-padding Vertical padding a step smaller than horizontal
Text carries its own line height above and below, so a box around text looks balanced with a
little less vertical padding than horizontal: px-4 py-3, not p-4.
zx/card-nesting Never put a card inside a card
Group content inside a card with headings, separators or a Description List. A card within a card doubles the borders and the padding and reads as a mistake.
// Good
<Card>
<CardHeader>
<CardTitle>Asset</CardTitle>
</CardHeader>
<CardContent>
<DescriptionList>...</DescriptionList>
</CardContent>
</Card>
// Avoid
<Card>
<CardContent>
<Card>...</Card>
</CardContent>
</Card>
zx/border-or-shadow A raised surface gets a ring, not a border and a shadow
Floating surfaces (popovers, menus, dialogs) use a shadow with a translucent ring, as the library
components do. A solid border under a shadow looks heavy. Surfaces on the page (cards, tables)
use a border and no shadow.
// Good
<div className="rounded-lg bg-popover shadow-md ring-1 ring-foreground/10">...</div>
// Avoid
<div className="rounded-lg border border-border bg-popover shadow-md">...</div>
zx/concentric-radius Nested corners are concentric
When a rounded element sits inside another with little padding between them, the inner radius is the outer radius minus that padding. Equal radii make the corners look uneven.
// Good
<div className="rounded-xl p-1">
<div className="rounded-lg">...</div>
</div>
// Avoid
<div className="rounded-xl p-1">
<div className="rounded-xl">...</div>
</div>
zx/icon-alignment Icons align with the first line of text
When text next to an icon can wrap, align the icon with the first line, not the middle of the block. Wrap it in an element one line tall.
// Good
<div className="flex items-start gap-2">
<span className="flex h-lh items-center">
<TriangleAlertIcon className="size-4" />
</span>
<p className="text-sm">A message that may wrap onto a second line.</p>
</div>
// Avoid
<div className="flex items-center gap-2">
<TriangleAlertIcon className="size-4" />
<p className="text-sm">A message that may wrap onto a second line.</p>
</div>
zx/inline-code Identifiers in running text are monospace and slightly smaller
Hashes, IPs, file names and keys inside a sentence use font-mono at about 90% of the text
size, so they do not look bigger than the words around them. Standalone values in tables and
detail views use font-mono at the normal size.
zx/sticky-border Sticky bars have a border
A header or toolbar that stays in place while content scrolls under it gets a border-b (or
border-t at the bottom), so the edge stays visible against the moving content.
zx/overlay-state Never render an overlay conditionally
Keep dialogs, sheets and popovers mounted and control them with open. Rendering them
conditionally skips the exit animation and loses focus restoration.
// Good
<Dialog open={open} onOpenChange={setOpen}>...</Dialog>
// Avoid
{open ? <Dialog open>...</Dialog> : null}
zx/collapse-size Content keeps its size while it collapses
When a panel animates closed, the content inside keeps its width or height and is clipped, rather than reflowing as the panel shrinks.
zx/heading-action An action beside a heading is centered on it
The action of a page header, a section heading or a card header sits on the right, vertically
centered on the heading block: the title, or the title and its description together.
Section Heading and CardAction do it for you; a hand-built header
uses items-center, not items-start.
zx/control-height Controls in one row share one size
Buttons, inputs, selects, toggle groups and filter controls placed side by side use the same
size, so they share one height: 28px (sm), 32px (default) or 36px (lg). A toolbar of
mixed heights looks broken even when each control is right on its own.
// Good
<Input size="sm" className="w-56" placeholder="Search alerts" />
<ToggleGroup variant="segmented" size="sm">...</ToggleGroup>
<Button variant="outline" size="sm">Export</Button>
// Avoid: 32px input, 28px toggle group, 32px button
<Input className="w-56" placeholder="Search alerts" />
<ToggleGroup variant="segmented" size="sm">...</ToggleGroup>
<Button variant="outline">Export</Button>
zx/selected-state Selected, hovered and checked look different
| State | Treatment |
|---|---|
| Hover on a control, a row or a navigation item | bg-surface-muted |
| Highlighted item of a menu, command list or listbox: the item Enter would choose | The selection colors below |
| Selected or pressed: a pressed toggle, a segmented choice, an active filter chip, the current sidebar item, tab | bg-primary-soft with text-primary-soft-foreground, and a primary border or underline where it has one |
| Checked form controls and chosen values: checkbox, radio, switch, the selected calendar day | The primary fill |
Use text-primary-soft-foreground, not text-primary, on bg-primary-soft: only the first
reaches 4.5:1 in both themes. A selected item never looks like a hovered one: where an item can
be both (a sidebar, a toggle), hover stays bg-surface-muted.
zx/one-primary One filled button per view
See Action placement.
zx/color-not-alone State is never shown by color alone
Every status, severity and validation color comes with text, and with an icon where space allows.