Layout patterns
Recommended compositions of existing components for the layouts UI work keeps reinventing. None needs a new component.
Balanced card rows
Side-by-side sections of equal height that stack on mobile. The kit has no row component, and needs none: a CSS grid stretches its items by default, and a Card is a flex column, so a card whose CardPanel is flex-1 fills the cell. Do not set heights by hand.
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Guest list</CardTitle>
<CardDescription>Who is coming.</CardDescription>
</CardHeader>
{/* CardPanel is flex-1: it grows, so both cards end at the same line */}
<CardPanel>…</CardPanel>
<CardFooter>
<Button variant="outline">Edit</Button>
</CardFooter>
</Card>
<Card>…</Card>
</div>Use grid-cols-1 (the default) below lg so cards stack. Use auto-rows-fr if rows of several cards must all match. For a row of KPIs use StatGroup, which already stacks on mobile.
Page section header
DashboardHeader is the page's title bar; SectionHeader is one level down and introduces a block that is not a card. Inside a Card, use CardHeader with CardAction instead.
<SectionHeader
title="Team members"
description="People who can manage dinners."
actions={<Button size="sm" variant="outline">Invite</Button>}
border
/>
<ItemGroup>…</ItemGroup>Its title is a div, not a heading element: wrap it in the heading your document outline needs. Pass icon for a leading FeaturedIcon.
Empty states
Put Empty directly in CardPanel (or DialogPanel) and it takes the surface's gutter; say what is missing and offer the next step. Use ErrorOrEmptyState and its ready-made variants for failures.
<Card>
<CardHeader><CardTitle>Events</CardTitle></CardHeader>
<CardPanel>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon"><CalendarPlusIcon /></EmptyMedia>
<EmptyTitle>No events yet</EmptyTitle>
<EmptyDescription>Plan your first dinner to get started.</EmptyDescription>
</EmptyHeader>
<EmptyContent><Button size="sm">Plan an event</Button></EmptyContent>
</Empty>
</CardPanel>
</Card>Do not add your own padding around it: the surface owns the gutter. While loading, show Skeleton, not Empty.
Form rows
One Field per control, stacked in a FieldGroup. For two controls on a row, a grid; it collapses to one column on mobile. Group related fields in a Fieldset (it is max-w-64 by default, so pass max-w-none).
<form className="contents">
<FieldGroup>
<div className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>First name</FieldLabel>
<FieldControl render={<Input />} />
<FieldError />
</Field>
<Field>
<FieldLabel>Last name</FieldLabel>
<FieldControl render={<Input />} />
<FieldError />
</Field>
</div>
<Field>
<FieldLabel>Tier</FieldLabel>
<Select items={tiers}>…</Select>
<FieldDescription>You can change this later.</FieldDescription>
</Field>
</FieldGroup>
<Button type="submit">Save</Button>
</form>A Card wrapped in a form loses the card's direct-child spacing rules: render the card as the form instead, <Card render={<form onSubmit={…} />}>. Dates and times use DatePicker / TimePicker, never native inputs. Keep filter pickers' options independent of the response they filter (see MultiSelect).
Traps
- Never pass data-slot to a kit component: it overwrites the kit's own and its rules stop applying. Tag with data-part.
- cn() reads text-xxs as a colour, so a colour class in the same cn() call is dropped. Put the colour on a parent.
- --brand fails contrast as text in both themes; use sidebar-primary for a legible gold.
- Numbers in a Card default to serif old-style figures; build metrics from Stat parts (StatValue is sans and tabular).