Skip to content

Spacing & layout

A 4-pixel grid, a short list of steps that do real work, and layout rules that keep density consistent from a settings form to a dashboard.

Consistent spacing is invisible when it works and obvious when it doesn't. prfct builds every component and pattern on a 4px grid and uses a deliberately short list of steps, each with a job.

Invite teammate

They'll get an email with a link.

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

function Gap({ size, px }: { size: string; px: number }) {
  return (
    <span
      aria-hidden="true"
      className="pointer-events-none absolute left-full ml-3 flex items-center gap-1.5 font-mono text-[0.625rem] text-brand-11"
      style={{ height: px }}
    >
      <span className="h-full w-px bg-brand-7" />
      {size}
    </span>
  )
}

export default function SpacingRhythm() {
  return (
    <div className="relative w-72 rounded-xl border bg-card p-6 shadow-raised">
      <div className="flex flex-col gap-1.5">
        <p className="text-heading-sm">Invite teammate</p>
        <p className="text-sm text-muted-foreground">
          They&apos;ll get an email with a link.
        </p>
      </div>
      <div className="relative h-6">
        <Gap size="6 · 24px" px={24} />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="rhythm-email">Email</Label>
        <Input id="rhythm-email" placeholder="ada@acme.co" />
      </div>
      <div className="relative h-6">
        <Gap size="6 · 24px" px={24} />
      </div>
      <div className="flex justify-end gap-2">
        <Button variant="outline" size="sm">
          Cancel
        </Button>
        <Button size="sm">Send invite</Button>
      </div>
    </div>
  )
}

The scale

Spacing uses Tailwind's numeric scale, where one unit is 4px (p-4 is 16px). The whole scale is available, but these steps carry the system:

2pxHairline gaps, icon nudges
4pxTight inline gaps
6pxIcon-to-label gap in small controls
8pxIcon-to-label gap, compact stacks
12pxControl padding, related items
16pxCard padding (compact), field stacks
20pxGroup spacing
24pxCard padding, section stacks
32pxSeparated groups
40pxPage gutters (desktop)
48pxSection spacing
64pxMajor sections
96pxMarketing section rhythm
128pxHero spacing

Rules of rhythm

Proximity is grouping. Related elements sit closer together than unrelated ones. A label is 8px from its input; fields are 20px apart; sections are 48px apart. If two things need a divider to look separate, they probably need more space instead.

Inside no larger than outside. Padding inside a container is at most the space around it, so the container reads as one group. Card pads its content by 24px, or 16px at size="sm", so cards in a grid sit at least that far apart.

Use gap, not margins. Components never set outer margins; parents arrange children with flex or grid gap. This keeps components reusable in any layout and removes margin-collapsing surprises.

// Preferred
<div className="flex flex-col gap-5">…</div>

// Avoid — spacing that belongs to the parent leaks into children
<div className="space-y-5">…</div>

Density

Buttons come in five heights. Inputs, selects and toggles share the middle three, sm, default and lg, so anything placed on a row aligns without effort:

SizeHeightUse
xs28pxDense tables and toolbars. Pointer-first UI only.
sm32pxSecondary controls, compact forms, card headers.
default36pxMost controls in product UI.
lg40pxPrimary forms, touch-first surfaces.
xl48pxMarketing calls to action.

Choose a density per surface, not per component: a data-heavy table page may use sm throughout; an onboarding flow may use lg.

Layout

Breakpoints

prfct uses mobile-first breakpoints: unprefixed classes apply everywhere, prefixed ones apply from that width up.

PrefixMin widthTarget
sm:40rem · 640pxLarge phones, landscape.
md:48rem · 768pxTablets, portrait.
lg:64rem · 1024pxTablets landscape, small laptops.
xl:80rem · 1280pxLaptops and desktops.
2xl:96rem · 1536pxLarge desktops.

Containers and gutters

  • Page gutters: 16px on phones, 24px from sm, 32–40px on wide screens.
  • Reading width: long text runs 45–75 characters per line (max-w-prose, max-w-2xl).
  • App shell: a fixed sidebar (240–260px) and a fluid content area; content pages center at max-w-5xl, data pages use the full width.

Container queries

Components that change layout based on their own size — not the viewport — use container queries. FieldGroup is a container, so a responsive field switches from stacked to side-by-side when its column is wide enough, whether that column is a full page or a narrow sheet.

<FieldGroup>
  <Field orientation="responsive">…</Field>
</FieldGroup>

Layering

Stacking is a token, too. Overlays portal to the end of the document and use the layer that matches their role, so a select opened inside a dialog always appears above it.

Tokenz-indexUse for
z-base0Document flow.
z-raised10Sticky table headers, floating action rails.
z-sticky20Sticky page headers and toolbars.
z-overlay40Modal scrims.
z-modal50Dialogs, sheets, drawers.
z-popover60Menus, popovers, selects (above modals).
z-toast70Notifications.
z-tooltip80Tooltips — always on top.