Free Tools
Design System Toolkit.
Five generators for the decisions that are tedious to make by hand. Pin one size you already know and the rest of the scale derives from it, in both directions. Every panel exports CSS custom properties, Tailwind v4 theme tokens, and Figma variables.
The exports are for the web: CSS custom properties, Tailwind theme tokens and Figma variables. The maths is not web-specific though, so the type scale also shows what each step maps to on iOS and Android, and the panels that genuinely do not port say so.
For the reasoning behind the defaults, read how to build a type scale: it covers why the golden ratio needs a square root, and specs every UI element from buttons to table cells.
If your H1 is 48px, what is everything else?
Pick the one size you have already decided, pin it, and the rest of the ladder derives up and down from it. The default ratio is the square root of the golden ratio, so every second step is a true 1.618 interval: you get a genuinely golden scale without the dead zone pure phi leaves between body copy and your smallest heading.
| Role | Preview | px | rem | Line height | Tracking | Weight |
|---|
On iOS and Android
Your scale against what each platform ships by default. The gap is the
useful part: near zero means you can use the platform's own style
untouched, a large number means you are defining a custom one. Points, dp
and CSS pixels are all density-independent, so the numbers compare
directly. Android sp also scales with the reader's font-size
preference, so treat that column as a starting point rather than a fixed size.
| Role | Yours | iOS Dynamic Type | Material 3 |
|---|
A grid that stays whole-pixel
Multiples of 4 survive every common device density, and 8 halves cleanly twice. The second table is the part most systems get wrong: the space above a heading has to beat the space below it, or the heading floats between two blocks instead of belonging to the one it introduces.
| Token | Scale | px | rem | Multiple |
|---|
Vertical rhythm
Space above a heading must exceed the space below it: a heading belongs to
the content that follows, so the gap separating it from the previous block
has to read as larger. A symmetric margin: 1em 0 is the usual
mistake. These use a 2:1 ratio, expressed in em so they scale
with each heading rather than needing a value per level.
| Element | Margin top | Margin bottom | At body size |
|---|
Enter one colour, get the ones that go with it
Combinations are generated in OKLCH, so every member holds the same perceived lightness. Rotating hue in HSL does not: at an identical HSL lightness, yellow and blue differ by nearly nine times in actual luminance, which is why so many generated palettes have one member you cannot read. Trend directions bias chroma and lightness only, so the colour you type always drives the result.
Worth knowing before you trust any of these: the classic wheel rules are art-school theory, not measured fact. What research there is finds that preference peaks in the contrast region around 160 to 220 degrees, which supports complementary, while harmony rises with hue similarity, which supports analogous. Those are different things. The same work also finds that hue independence does not hold: blues and orange-yellows combine well, reds, greens and purples less so, whatever the geometry says.
The combination
The four roles
The short version, if you think in main, accent, neutral and contrast. Everything here is drawn from the scale below, with each pairing already checked for contrast rather than left to the eye.
As a UI palette
Twelve steps where each one has a job, rather than a lightness number you have to reinterpret for every hue. Step 9 is the chroma peak, which is why it is the natural brand solid. Greys are tinted with your hue at very low chroma, which is most of what stops a generated palette looking generated.
| Step | Job | Swatch | Hex | Contrast | Perceptual |
|---|
How much of each
The 60-30-10 split, with one correction. Your colour is not the 60%: at full chroma across most of a page it is overwhelming. The 60% is your hue at near-neutral chroma, which is what steps 1 and 2 already are. The accent only works as a call to action while it stays scarce.
60-30-10 comes from interior design, not screen design. Treat it as an area check rather than a formula: a real system has two surface tiers, several border and text tiers and a set of semantic scales, so three colours was never the whole model.
Full scale
| Step | Swatch | Hex | On white | On black | Readable text |
|---|
Trend directions compiled August 2026. They bias chroma and lightness only: the hue is always yours. Forecasters disagree with each other in any given year, so treat these as directions rather than facts.
Judge these by eye, not by number
Both ramps are rendered rather than listed, because nobody has ever picked a shadow off a spreadsheet. Each elevation is drawn in two layers: a tight contact shadow that anchors the element, and a wider ambient one that carries its height.
Radius and elevation are the cheapest parts of a system to derive and the fastest to judge, so both are rendered here rather than listed. Shadows are drawn in two layers: a tight contact shadow that anchors the element, and a wider ambient one that carries the height.
Radius
Elevation
Shown on a light canvas on purpose: a black shadow cast onto a near-black surface has nowhere to go, so dark interfaces generally signal height by lightening the surface instead of deepening the shadow.
Radius travels: the same number is dp on Android and points on iOS. The
shadows do not. Android Material takes a single elevation value
in dp and draws the shadow for you, so a two-layer CSS shadow has no direct
equivalent: match the level, not the offsets. iOS gives you the layer
properties individually, as shadowOffset,
shadowRadius and shadowOpacity, so the two layers
here would need two views or a redraw.
One value that scales between two viewports
The slope is computed in pixels and only the intercept is converted to rem, which is where most published clamp examples go wrong by a factor of sixteen. Keeping a rem term in the preferred value is not stylistic: a pure viewport value cannot respond to browser text sizing, and that is a named WCAG failure.
Web only, unavoidably. Neither iOS nor Android has a viewport-relative unit,
so there is no clamp() equivalent to port. iOS scales type
through Dynamic Type and adapts layout with size classes; Android uses
resource qualifiers and window size classes. The idea of interpolating
between two anchor sizes carries over; this syntax does not.
How it is derived
Rendered across the range
| Viewport | Computed size | Preview |
|---|
One source, three places it has to land
A design system fails at the handoff, not at the maths. The panels export the same values three ways so the stylesheet, the Tailwind theme and the Figma file start from one set of numbers rather than three that agree on Monday and disagree by Friday.
CSS custom properties
Paste into :root. Tokens, not utilities, so they work
in plain CSS, in a component library, and inside a framework you
have not chosen yet.
Tailwind v4 theme tokens
Tailwind v4 reads its theme from CSS, so these go straight into
@theme and the utility classes generate themselves. No
config file to keep in sync.
Figma variables
The same values as collections, so a designer picking a size in Figma and a developer typing a token name are choosing from one list. This is the export that stops the drift.
The maths is not web-only. The type scale shows what each step maps to in iOS Dynamic Type and on Android, and the panels whose output does not port cleanly to native say so rather than quietly handing you a number that means something else there.
FAQ
Frequently asked questions
What base size should I pin?
Sixteen pixels, unless you have a reason. It is the default text size in every desktop browser, so a user who has raised or lowered that default gets a scale that moves with them. Pinning the base is the one decision worth making by eye: set the size your body copy has to be at arm’s length on a real screen, then let the ladder derive from it rather than choosing seven sizes independently.
Which ratio should I choose?
The default here is 1.272, the square root of the golden ratio, because every second step is then a true 1.618 interval. Pure phi is too coarse for interface work: it leaves a gap between body copy and the smallest heading with nothing usable in it. Denser products want a smaller ratio around 1.2; editorial pages can carry 1.333 or more. If you are unsure, pick the smaller one — a scale that is too tight is a smaller problem than one with unusable steps.
How many steps do I actually need?
Six to nine. Fewer and you start reaching for sizes outside the system; more and two adjacent steps become close enough that nobody can tell you which to use, so the scale stops making the decision for them. Every step should have a job you can name.
Should type sizes be in px or rem?
Rem, for anything a person reads. A rem is relative to the browser’s root size, so a reader who has increased their default text size gets larger text; pixel sizes ignore that entirely, and ignoring it is a WCAG failure, not a preference. Think in pixels while designing if that is more natural, then ship rem — the exports here do the conversion.
How do I make the scale responsive without a second set of sizes?
Use the fluid panel rather than redeclaring every size at every breakpoint. It builds a clamp() value that moves smoothly between two viewports, which means one declaration instead of three. The important detail is that the preferred value keeps a rem term: a pure viewport unit cannot respond to browser text sizing at all, which is the failure most published clamp snippets ship with.
Why does my generated palette have one colour nobody can read?
Because it was almost certainly generated by rotating hue in HSL, where lightness is a coordinate rather than a perception. At identical HSL lightness, yellow and blue differ by nearly nine times in actual luminance. The colour panel works in OKLCH instead, so every member of a combination holds the same perceived lightness and the readable one is not a matter of luck.
Why is the space above a heading larger than the space below it?
Because a heading belongs to the block it introduces. If the two gaps are equal, or the one below is larger, the heading floats between two sections and the reader has to work out which one it labels. The spacing panel treats that asymmetry as a rule rather than a judgement call.
Can I get these tokens into Tailwind or Figma?
Yes — every panel exports CSS custom properties, Tailwind v4 theme tokens and Figma variables from the same values, so the design file and the stylesheet start from one source instead of two that drift. The type scale additionally maps each step to its iOS and Android equivalent, and the panels whose maths genuinely does not port to native say so rather than pretending.