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'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:
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:
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.
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.