Spacing
Every spacing value in azucar is a multiple of 4px. azucar.css remaps the whole Tailwind spacing scale to that grid:
@theme inline {--spacing: 4px;}
So mt-1 is 4px, p-2 is 8px, gap-6 is 24px, and mt-25 is 100px. There is no 6px and there is no 10px, anywhere.
Why one grid
- Alignment without coordination. Every margin, padding, and gap comes from one ladder, so unrelated components line up when you compose them. Nobody negotiates pixel offsets across card boundaries.
- Smaller decisions. To choose spacing, you pick a step from a short ladder, not a value from a continuum. Two engineers given the same layout land on the same classes.
- Mechanical review. Any off-grid value is wrong by definition, so the linter owns that argument, not the reviewer.
Proximity is meaning
The grid gives the steps. This section tells you how to choose one. The rule: the closer two elements sit, the more related they are. Spacing is a statement about relationship. A small gap says: one thing. A large gap says: a new thing starts here.
Space by groups, not by eye. Name the groups first. Use the smallest steps inside a group and clearly larger steps between groups. Keep the order strict: more related is always closer. A layout that breaks this order is wrong, even when every value sits on the grid.
The cards below show the difference. A title and its subtitle describe one thing. The action does a different job. Equal gaps claim that all three are equally related, so you must read to find the structure. The good card binds the pair with 4px and pushes the action away with 24px. You see the groups before you read a word. The gray bars are the gaps, drawn at true size. Edit the values, or turn the checkbox off to see the finished cards.
<div className="flex flex-col gap-10"> <div> <div className="mb-2 font-mono text-xs text-fg-2">bad: 12 / 12</div> <Card className="w-72 p-4"> <div className="font-medium text-fg-4">Verified accuracy</div> <Gap px={12} /> <div className="text-sm text-fg-2">across 367 records</div> <Gap px={12} /> <Button variant="ghost">Reconcile</Button> </Card> </div> <div> <div className="mb-2 font-mono text-xs text-fg-2">good: 4 / 24</div> <Card className="w-72 p-4"> <div className="font-medium text-fg-4">Verified accuracy</div> <Gap px={4} /> <div className="text-sm text-fg-2">across 367 records</div> <Gap px={24} /> <Button variant="ghost">Reconcile</Button> </Card> </div> </div>
In a real component
RankedTable shows the whole ladder inside one component. Rows sit 4px apart: they are the most similar elements in the card, one kind repeated. Columns sit 12px apart: the cells of a row belong to one record, but each column is a different kind of fact, so columns get the larger step. The column names sit 16px below the title: the title names the card, the column names describe the data, and each level of separation gets its own step. Inside a row, the label and the value sit far apart but read as one fact, because the shared row binds them. Proximity is the main tool, not the only one. Sort the table and watch the rows trade places. The rhythm does not change. The overlay marks the measured gaps. The checkbox turns it off.
<Xray> <Card className="flex h-90 w-full flex-col overflow-hidden"> <RankedTable ariaLabel="Facilities by power gap" header="Discrepancies" defaultSort={{ columnKey: "gap", direction: "desc" }} rowKey={(row) => row.id} categories={[ { key: "facilities", label: "Facilities", rows: [ { id: "tor5", name: "TOR5", records: 367, gap: 134.3 }, { id: "atl2", name: "ATL2", records: 298, gap: 88.1 }, { id: "dfw1", name: "DFW1", records: 512, gap: 61.4 }, { id: "phx4", name: "PHX4", records: 120, gap: 22.9 }, { id: "sea3", name: "SEA3", records: 84, gap: 8.2 }, ], columns: [ { key: "name", header: "Facility", cell: (row) => row.name }, { key: "records", header: "Records", cell: (row) => row.records, sortBy: (row) => row.records, width: "88px", }, { key: "gap", header: "Power gap", cell: (row) => row.gap + " kW", sortBy: (row) => row.gap, width: "96px", }, ], }, ]} /> </Card> </Xray>
Enforcement
Off-grid spacing is a lint error. bun run build runs lint first, so an off-grid value cannot reach a bundle. eslint-plugin-better-tailwindcss restricts every m, p, gap, and space utility:
Off-grid spacing is not allowed. Use a whole-number Tailwind token based on 4px, such as mt-1 (4px), mt-2 (8px), or mt-25 (100px). Fractional, arbitrary, and px-based spacing values are forbidden.
The banned forms: fractional steps (p-0.5, gap-1.5), arbitrary values (mt-[10px], p-(--foo)), fractions (gap-x-1/2), and the pixel token (m-px). Variants do not Escape the rule. hover:mt-[10px] fails the same way.
Do and don't
// do: whole-number steps on the grid<div className="mt-6 flex flex-col gap-3 p-4" />// don't: any of these are build errors<div className="mt-[10px] gap-1.5 p-px" />
If a layout seems to need an off-grid value, the layout is fighting the system. Use a different step, or raise it with Inacio. Do not work around the rule.