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).