Proportion system
Spacing
Eleven spacing steps taken from the Fibonacci sequence on a 16 px base, with the corner radii that go with them.
The sequence
Each Fibonacci number is the sum of the two before it:
Draw a square for each term and wind them around one another, and the corners of the squares trace a spiral. It is the same shape as a nautilus shell, a sunflower head or the arms of a galaxy: growth in which each step is a fixed proportion of the last.
Why it works
The ratio of neighbouring terms settles on the golden ratio, so any two adjacent steps in the scale feel related:
| n | F(n) | F(n) / F(n-1) | Error |
|---|---|---|---|
| 2 | 1 | 1.000000 | -0.618034 |
| 3 | 2 | 2.000000 | +0.381966 |
| 4 | 3 | 1.500000 | -0.118034 |
| 5 | 5 | 1.666667 | +0.048633 |
| 6 | 8 | 1.600000 | -0.018034 |
| 7 | 13 | 1.625000 | +0.006966 |
| 8 | 21 | 1.615385 | -0.002649 |
| 9 | 34 | 1.619048 | +0.001014 |
| 10 | 55 | 1.617647 | -0.000387 |
| 11 | 89 | 1.618182 | +0.000148 |
| 12 | 144 | 1.617978 | -0.000056 |
| 13 | 233 | 1.618056 | +0.000022 |
The first few steps are coarse (1, 2, 3, 5 differ by 100%, 50%, 67%) and by the tenth term the ratio is within a thousandth of phi. That matches how spacing is used: small steps are fine adjustments and large steps separate regions.
The scale
The base is 16 px, which is 1 rem on the web and 16 dp in Flutter. Every step is a whole number of pixels, and each token is a multiple of the 16 px unit, so the web and Flutter agree to the pixel. Tailwind reads the tokens as p-f13, gap-f8 and so on; the number is the pixel value.
| Token | px | rem | Step | Typical use |
|---|---|---|---|---|
--spacing-f1 | 1 | 0.0625 | Hairline border | |
--spacing-f2 | 2 | 0.125 | Focus ring, divider weight | |
--spacing-f3 | 3 | 0.1875 | Optical nudge | |
--spacing-f5 | 5 | 0.3125 | Icon to label gap, chip padding | |
--spacing-f8 | 8 | 0.5 | Inline gap, small control padding | |
--spacing-f13 | 13 | 0.8125 | Stack gap, compact card padding | |
--spacing-f21 | 21 | 1.3125 | Card padding, group gap | |
--spacing-f34 | 34 | 2.125 | Screen gutter, loose padding | |
--spacing-f55 | 55 | 3.4375 | Section gap | |
--spacing-f89 | 89 | 5.5625 | Hero gap | |
--spacing-f144 | 144 | 9 | Page-level gap |
Corner radii
Radii use the same numbers. Anything fully rounded is a pill and uses the platform's full-round shape instead of a number.
Rules
- Use only these steps for padding, margin, gap, offsets and radii. Never write a pixel value that is not on the scale.
- Neighbours inside, a step up outside. Space between related things is one step smaller than the space around the group. Padding inside a card is 21, the gap between cards is 21 or 34, and the gap between sections is 55.
- Skip a step to make a difference visible. If two gaps need to read as different, use steps that are two apart (8 and 21), not neighbours (13 and 21).
- Never subdivide a step. If a gap needs half of 13, the layout needs a different structure, not a new number.
- Do not carry meaning through space alone. Separation also needs a visible boundary, label or alignment, because spacing is lost when text is enlarged.
Usage
<!-- Tailwind: the number is the pixel value --><div class="grid gap-f21 p-f21 rounded-f21"> <p class="mb-f13">…</p></div>/* the equivalent plain CSS */.card { padding: var(--spacing-f21); border-radius: var(--radius-f21);}@theme static { --spacing-f1: calc(var(--u) * 0.0625); --spacing-f2: calc(var(--u) * 0.125); --spacing-f3: calc(var(--u) * 0.1875); --spacing-f5: calc(var(--u) * 0.3125); --spacing-f8: calc(var(--u) * 0.5); --spacing-f13: calc(var(--u) * 0.8125); --spacing-f21: calc(var(--u) * 1.3125); --spacing-f34: calc(var(--u) * 2.125); --spacing-f55: calc(var(--u) * 3.4375); --spacing-f89: calc(var(--u) * 5.5625); --spacing-f144: calc(var(--u) * 9); --radius-f3: calc(var(--u) * 0.1875); --radius-f5: calc(var(--u) * 0.3125); --radius-f8: calc(var(--u) * 0.5); --radius-f13: calc(var(--u) * 0.8125); --radius-f21: calc(var(--u) * 1.3125); --radius-f34: calc(var(--u) * 2.125); --radius-full: calc(infinity * 1px);}/// Astr spacing: Fibonacci numbers on a 16 dp base.////// Steps are 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 and 144 logical pixels./// Consecutive steps tend to the golden ratio, so any two neighbours look/// related. The constants mirror the `--spacing-f*` values in `scale.css`/// (1 rem = 16 dp), so web and Flutter lay out identically.////// Usage: `const EdgeInsets.all(AstrSpace.f21)`abstract final class AstrSpace { static const double f1 = 1; static const double f2 = 2; static const double f3 = 3; static const double f5 = 5; static const double f8 = 8; static const double f13 = 13; static const double f21 = 21; static const double f34 = 34; static const double f55 = 55; static const double f89 = 89; static const double f144 = 144; /// Every step in ascending order. static const List<double> steps = <double>[ f1, f2, f3, f5, f8, f13, f21, f34, f55, f89, f144, // ];}/// Astr corner radii: Fibonacci numbers, mirrored from `--radius-f*`./// Pills use a fully rounded shape (`StadiumBorder`), not a number.abstract final class AstrRadius { static const double f3 = 3; static const double f5 = 5; static const double f8 = 8; static const double f13 = 13; static const double f21 = 21; static const double f34 = 34;}