Skip to content

Spacing

A 4px rhythm with named gaps for controls, blocks, cards, gutters and sections.

A 4px base#

Every gap, padding and height is a multiple of 4px, with 6px as the one half step. The scale is Tailwind's own, so the token name is the class number.

The spacing scale
TokenSizeScaleClassesUsed for
14pxgap-1 p-1Menu padding, and tight stacks such as a name over its meta.
28pxgap-2 p-2 px-2Between inline items and buttons, list inset inside cards, grid cell padding, sidebar item padding.
312pxgap-3 px-3Between related blocks, app header side padding, well padding across.
416pxgap-4 p-4Card and dialog padding, rail gaps, gaps between cards.
624pxpx-6 gap-6Page gutters from 640px.
832pxpy-8 gap-8Page top and bottom padding, between page sections, between Home's columns.
1.56pxgap-1.5 px-1.5Icon to label inside controls, a status dot to its label, tag padding, page actions in the header.

The scale is read from the DESIGN.md frontmatter. Bars are drawn at twice their size. Tailwind's spacing unit is 0.25rem, so gap-3 is 12px and h-9 is 36px; there is no custom spacing token to import.

Named gaps#

Five distances cover almost every layout decision. Pick by relationship, not by eye.

Named gaps
RelationshipGapClasses
Inside controls and between inline items: icon to label, buttons in a row, a dot and its text6 to 8pxgap-1.5 gap-2
Related blocks: a section header and its content, a label and its field group12pxgap-3
Card padding, and gaps between rail cards16pxp-4 gap-4
Page gutters, 16px below 640px24pxpx-4 sm:px-6
Page sections, page top and bottom padding32pxgap-8 py-8

A card with its spacing drawn

Every distance in this payment run card is one of the named gaps. Switch the view to hide the annotations.

16

Friday payment run

3 suppliers
8
  • Northwind Freight$12,480.00
  • Halcyon$8,212.75
  • Orchard Street$1,904.50

16px card padding and before the footer, 12px between the header and its list, 8px list inset and between buttons, 6px from icon to label.

import { Button } from "@oration/canon/components/button";import { SegmentedControl } from "@oration/canon/components/segmented-control";import { toast } from "@oration/canon/components/toast";import { cn } from "@oration/canon/lib/utils";import { CheckIcon, FileTextIcon } from "lucide-react";import * as React from "react";export function AnnotatedCard() {    const [view, setView] = React.useState<"card" | "spacing">("spacing");    const on = view === "spacing";    const hatch: React.CSSProperties | undefined = on        ? {              backgroundImage:                  "repeating-linear-gradient(135deg, var(--border-strong) 0 1px, transparent 1px 6px)",          }        : undefined;    const chip =        "pointer-events-none absolute z-10 rounded-sm bg-foreground px-1 text-2xs leading-4 font-medium text-background tabular-nums";    const rows = [        { supplier: "Northwind Freight", amount: "$12,480.00" },        { supplier: "Halcyon", amount: "$8,212.75" },        { supplier: "Orchard Street", amount: "$1,904.50" },    ];    return (        <div className="flex w-full flex-col items-center gap-4">            <SegmentedControl                label="Card view"                value={view}                onValueChange={setView}                options={[                    { value: "card", label: "Card" },                    { value: "spacing", label: "Show spacing" },                ]}            />            <div                className="relative w-full max-w-sm rounded-xl bg-card p-4 shadow-border"                style={hatch}            >                {on ? (                    <span                        className={cn(chip, "top-0 left-1/2 -translate-x-1/2")}                    >                        16                    </span>                ) : null}                <div className="flex flex-col bg-card">                    <div className="flex items-baseline justify-between gap-3">                        <p className="text-sm font-semibold text-foreground">                            Friday payment run                        </p>                        <span className="text-xs text-muted-foreground">                            3 suppliers                        </span>                    </div>                    <div                        aria-hidden="true"                        className="relative h-3"                        style={hatch}                    >                        {on ? (                            <span                                className={cn(                                    chip,                                    "top-1/2 right-0 -translate-y-1/2",                                )}                            >                                12                            </span>                        ) : null}                    </div>                    <div className="relative rounded-[10px] bg-muted/70 p-2">                        {on ? (                            <span                                className={cn(                                    chip,                                    "top-0 left-0 -translate-y-1/2",                                )}                            >                                8                            </span>                        ) : null}                        <ul className="flex flex-col">                            {rows.map((row) => (                                <li                                    key={row.supplier}                                    className="flex h-8 items-center justify-between rounded-md px-2 text-13 hover:bg-muted"                                >                                    <span className="flex min-w-0 items-center">                                        <FileTextIcon                                            aria-hidden="true"                                            className="size-3.5 shrink-0 text-muted-foreground"                                        />                                        <span                                            aria-hidden="true"                                            className="relative h-3.5 w-1.5 shrink-0"                                            style={hatch}                                        />                                        <span className="truncate text-foreground">                                            {row.supplier}                                        </span>                                    </span>                                    <span className="font-medium text-foreground tabular-nums">                                        {row.amount}                                    </span>                                </li>                            ))}                        </ul>                    </div>                    <div                        aria-hidden="true"                        className="relative h-4"                        style={hatch}                    >                        {on ? (                            <span                                className={cn(                                    chip,                                    "top-1/2 right-0 -translate-y-1/2",                                )}                            >                                16                            </span>                        ) : null}                    </div>                    <div className="flex items-center justify-end">                        <Button type="button" variant="outline">                            Edit run                        </Button>                        <span                            aria-hidden="true"                            className="relative h-8 w-2 shrink-0"                            style={hatch}                        >                            {on ? (                                <span                                    className={cn(                                        chip,                                        "-top-2 left-1/2 -translate-x-1/2",                                    )}                                >                                    8                                </span>                            ) : null}                        </span>                        <Button                            type="button"                            onClick={() =>                                toast.add({                                    type: "success",                                    title: "Payment run approved",                                    description:                                        "3 suppliers will be paid on Friday, Oct 2.",                                })                            }                        >                            <CheckIcon                                data-icon="inline-start"                                aria-hidden="true"                            />                            Approve run                        </Button>                    </div>                </div>            </div>            {on ? (                <p className="max-w-sm text-center text-xs text-muted-foreground">                    16px card padding and before the footer, 12px between the                    header and its list, 8px list inset and between buttons, 6px                    from icon to label.                </p>            ) : null}        </div>    );}

Control heights#

Controls are 32px. Toolbars use 28px, the smallest inline actions 24px, and touch-first layouts 36px.

Four control heights

Buttons, inputs, selects, segmented controls and filter chips share these heights, so controls in a row line up without extra classes.

24pxh-6
28pxh-7
32pxh-8
36pxh-9
import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function ControlHeights() {    const sizes = [        { size: "xs", height: 24, className: "h-6" },        { size: "sm", height: 28, className: "h-7" },        { size: "default", height: 32, className: "h-8" },        { size: "lg", height: 36, className: "h-9" },    ] as const;    return (        <div className="flex flex-wrap items-end justify-center gap-x-8 gap-y-6">            {sizes.map((item) => (                <div                    key={item.size}                    className="flex flex-col items-center gap-3"                >                    <div className="flex items-end gap-2">                        <span                            aria-hidden="true"                            className="w-px bg-foreground/40"                            style={{ height: item.height }}                        />                        <Button                            type="button"                            variant="outline"                            size={item.size}                            onClick={() =>                                toast.add({                                    title: `${item.height}px control`,                                    description: `Button size "${item.size}", class ${item.className}.`,                                })                            }                        >                            Export                        </Button>                    </div>                    <div className="flex flex-col items-center">                        <span className="text-13 font-medium text-foreground tabular-nums">                            {item.height}px                        </span>                        <code className="font-mono text-xs text-muted-foreground">                            {item.className}                        </code>                    </div>                </div>            ))}        </div>    );}
Control heights
HeightClassUsed for
24pxh-6Extra-small buttons, tiny row actions, the large tag. The minimum hit target.
28pxh-7Toolbar controls, view tabs, filter chips, segmented controls and sidebar items.
32pxh-8The default: buttons, inputs, selects, the tabs track, the sidebar search field.
36pxh-9Large buttons and touch-first layouts. 36px also matches the default grid row.

Row heights#

Grid rows are 32, 36 or 44px by density, 36px by default. Headers, nav items and the app chrome have fixed heights of their own.

Grid density

The same invoices at each density. Headers stay 32px; only the rows change.

36px rows
Open invoices
InvoiceSupplierDueAmount
INV-20931Northwind FreightOct 2$12,480.00
INV-20944HalcyonOct 5$8,212.75
INV-20957Orchard StreetOct 9$1,904.50
INV-20960Brightline LogisticsOct 12$711.17
import { SegmentedControl } from "@oration/canon/components/segmented-control";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function RowDensity() {    const [density, setDensity] = React.useState<        "compact" | "default" | "comfortable"    >("default");    const rowHeight = { compact: "h-8", default: "h-9", comfortable: "h-11" }[        density    ];    const px = { compact: 32, default: 36, comfortable: 44 }[density];    return (        <div className="flex w-full flex-col items-center gap-4">            <div className="flex flex-wrap items-center justify-center gap-3">                <SegmentedControl                    label="Row density"                    value={density}                    onValueChange={setDensity}                    options={[                        { value: "compact", label: "Compact" },                        { value: "default", label: "Default" },                        { value: "comfortable", label: "Comfortable" },                    ]}                />                <span className="text-13 text-muted-foreground tabular-nums">                    {px}px rows                </span>            </div>            <div className="w-full max-w-xl overflow-x-auto rounded-xl bg-card shadow-border">                <table className="w-full min-w-[28rem] border-collapse text-13">                    <caption className="sr-only">Open invoices</caption>                    <thead>                        <tr>                            {["Invoice", "Supplier", "Due", "Amount"].map(                                (column) => (                                    <th                                        key={column}                                        scope="col"                                        className={cn(                                            "h-8 border-b border-border px-2 text-left font-medium whitespace-nowrap text-muted-foreground first:pl-3 last:pr-3",                                            column === "Amount" && "text-right",                                        )}                                    >                                        {column}                                    </th>                                ),                            )}                        </tr>                    </thead>                    <tbody>                        {invoices.map((invoice) => (                            <tr                                key={invoice.id}                                className="border-b border-border last:border-b-0 hover:bg-surface"                            >                                <td                                    className={cn(                                        "pl-3 font-mono text-xs text-foreground",                                        rowHeight,                                    )}                                >                                    {invoice.id}                                </td>                                <td className="px-2 text-foreground">                                    {invoice.supplier}                                </td>                                <td className="px-2 text-muted-foreground tabular-nums">                                    {invoice.due}                                </td>                                <td className="pr-3 text-right font-medium text-foreground tabular-nums">                                    {invoice.amount}                                </td>                            </tr>                        ))}                    </tbody>                </table>            </div>        </div>    );}
Fixed heights in the shell and in data
ElementHeightClass
App header48pxh-12
Data grid header32pxh-8
Grid row, compact32pxh-8
Grid row, default36pxh-9
Grid row, comfortable44pxh-11
Record rows in lists and cards, at least32pxmin-h-8
Sidebar item28pxh-7
Sticky footer of a full-bleed tool36pxh-9
Tag (large tag)20px (24px)h-5 (h-6)

Two components ship taller rows than the system. Table headers are 40px where grid headers are 32px, and SidebarMenuButton is 32px where sidebar items are 28px. Both product sidebars and most tables override them. See Table and Sidebar.

Density#

Oration is dense on purpose, and the room stays calm because the spacing is predictable.

  • Dense UI is 13px text in 32 to 36px rows. Reading text is 14px with room around it. Don't mix the two in one list.
  • Card padding is 16px at every size. A bigger card gets more content, not more padding.
  • Lists inside cards inset 8px, with 32px rows on 8px corners that fill Well Gray on hover.
  • Spacing grows with the distance in meaning: 6 to 8px within a thing, 12px between related things, 16px between cards, 32px between sections.
  • One density per grid. The density switch belongs to the view, not to individual rows.
  • Use gap on a flex or grid parent rather than margins on children, so a hidden child doesn't leave a stray gap.

W-9 status

Halcyon

On file since Sep 21

Do. Build from the named gaps: 16px padding, 12px between the heading and its well, 8px button gaps.

W-9 status

Halcyon

On file since Sep 21

Don't. Nudge each distance by eye with arbitrary values. Every card ends up a little different and the page stops reading as one system.

In Tailwind#

The recipes below cover a card and a document page. Copy them rather than restating the numbers.

Spacing recipes
// A card: 16px padding, 12px between related blocks<div className="flex flex-col gap-3 rounded-xl bg-card p-4 shadow-border">  <div className="flex items-baseline justify-between gap-3">    <h2 className="text-sm font-semibold">Friday payment run</h2>    <Link className="text-xs text-muted-foreground" href="/payments/runs/42">View run</Link>  </div>  {/* A list inset 8px, rows 32px with 8px corners */}  <ul className="flex flex-col rounded-[10px] bg-muted/70 p-2">    <li className="flex h-8 items-center gap-1.5 rounded-md px-2 text-13">…</li>  </ul>  <div className="flex justify-end gap-2">…</div></div>// A document page: 24px gutters (16px below 640px), 32px top and bottom,// sections 32px apart<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6">  <div className="flex flex-col gap-8">…</div></div>