Stripe's two design systems, and why they disagree

Search for Stripe's design system and you will find people looking for the wrong thing. Stripe's internal system is not published. What is published is two customer-facing systems that make exactly opposite bets about who is allowed to control the pixels — and the reason they disagree is worth more than any token file.

Updated July 27, 2026

Independent analysis of Stripe's public developer documentation, written for people designing their own systems. Identity Forge is not affiliated with or endorsed by Stripe. Stripe and its logo are trademarks of their owner. Details here reflect the documentation at time of writing; verify against docs.stripe.com before building on any specific value.

The thing people are searching for does not exist publicly

Stripe's own product surfaces — the Dashboard, the marketing site, the docs — run on an internal system that has been referred to over the years by various names and has never shipped as a public package. There is no npm install, no Storybook, no token export. If that is what you came for, it is not available, and third-party reconstructions of it are inference from screenshots.

What Stripe does publish, and documents thoroughly, is two systems aimed at developers integrating with Stripe. They are usually discussed separately because they live in different corners of the docs. Read together, they are a small masterclass in design system boundaries.

Stripe Apps UI toolkitElements Appearance API
Where the UI rendersInside the Stripe DashboardInside your product
Whose brand governsStripe'sYours
Arbitrary CSSNot possible — the css prop takes tokens onlySupported, via the rules map
SpacingSeven fixed named stepsOne spacingUnit you set; everything derives
Component stylingPreset; some components refuse overrides entirelyThemeable down to individual selectors and states
What it optimises forConsistency across thousands of third-party appsBlending into a checkout you did not design
Two public Stripe systems, two opposite postures.

The line between them is not technical. It is a trust boundary. When your UI sits on Stripe's surface, Stripe cannot allow a badly-styled third-party app to make the Dashboard look broken, so it removes the capability. When Stripe's UI sits on your surface, a payment form that ignores your design would look like a security problem, so Stripe hands the controls over. Same company, same design values, opposite defaults — because the question is not "what should this look like" but "who is accountable for how it looks".

Stripe Apps: a design system enforced by the type signature

The Stripe Apps UI toolkit gives you a component set — views, layout, navigation, content, forms, charts — plus a Box primitive with a css prop. The css prop looks like a styling escape hatch and is not one. It accepts named tokens, and nothing else.

<Box css={{
  stack: 'y',
  gap: 'medium',
  padding: 'large',
  backgroundColor: 'surface',
  borderRadius: 'medium',
}} />

// gap: 'medium'  ✅  a token
// gap: '17px'    ❌  not in the vocabulary

The spacing vocabulary is seven steps and a zero, documented with fixed pixel values: xxsmall 2px, xsmall 4px, small 8px, medium 16px, large 24px, xlarge 32px, xxlarge 48px. Sizing is fractional — halves, thirds, quarters, fifths, sixths, twelfths, plus fill — with content-based min, max and fit options.

Note the ratios in that spacing scale. It is not linear (2, 4, 8, 16, 24, 32, 48): it doubles at the small end where differences of 2px are visible, then steps by 8 at the large end where they are not. A scale that doubles all the way to 128 wastes steps nobody uses; one that steps by 4 the whole way gives you thirteen values that all look the same.

The interesting part is what happens above Box. Components other than Box and Inline carry preset styles, and the documentation is explicit that some of them cannot be overridden at all — a component that changes appearance based on which callbacks it implements will not let you contradict that, because the appearance is carrying meaning. Others expose a small enum: Button has primary, default and destructive, and that is the entire list. A few expose one property, like Icon accepting fill.

There is also a documented set of component hierarchy constraints — rules about which components may contain which. That is the layout equivalent of the same idea: not advice about good structure, but a restriction that makes bad structure fail rather than ship.

The lesson: enforcement beats documentation

Almost every design system is a documentation project. It says use the tokens, and then ships a className prop that accepts anything, and six months later half the codebase has arbitrary hex values in it because someone was in a hurry on a Friday.

Stripe Apps removes the option. There is no path from "in a hurry" to #3c82f6, because the prop will not take it. That is a much stronger guarantee than a linting rule and an infinitely stronger one than a paragraph in a wiki. If you maintain a system that has to survive contributors who did not read the docs — which is all of them — this is the move.

It is also the move with the highest cost, and worth being honest about that. A closed vocabulary means every genuinely new requirement becomes a request to the system owners. That is tolerable when the owners are a funded platform team serving an app marketplace. It is miserable when it is one person maintaining a system for four product squads who all have deadlines.

Elements: the three-tier ladder

The Appearance API solves the opposite problem. A payment form has to feel native to a site nobody at Stripe has seen, while keeping a layout Stripe controls for conversion and compliance reasons. Stripe's answer is a ladder with three rungs, and the documentation tells you to climb it in order.

  1. 1

    Pick a theme

    Three prebuilt starting points — stripe, night, flat. One line, and most integrations that only need to not-clash are done here.

    const appearance = { theme: 'night' }
  2. 2

    Set variables

    A small set of values that propagate everywhere. This is the token layer, and it is where the majority of real customisation happens.

    const appearance = {
      theme: 'stripe',
      variables: {
        colorPrimary: '#0570de',
        colorBackground: '#ffffff',
        colorText: '#30313d',
        colorDanger: '#df1b41',
        fontFamily: 'Ideal Sans, system-ui, sans-serif',
        spacingUnit: '2px',
        borderRadius: '4px',
      },
    }
  3. 3

    Add rules, only if you still need to

    A map of CSS-like selectors to CSS properties, reaching individual components and states. This is the escape hatch, and it is deliberately last.

    const appearance = {
      rules: {
        '.Tab': { border: '1px solid #E0E6EB' },
        '.Tab:hover': { color: 'var(--colorText)' },
        '.Tab--selected': { borderColor: '#E0E6EB' },
      },
    }

The ordering is the design. Each rung is more powerful and more expensive to maintain than the one below it, and the documentation pushes you down the ladder rather than up it. A team that starts at rules writes forty selectors and owns them forever; a team that starts at theme writes one line and only descends when it actually has to.

spacingUnit is the token-design idea worth stealing

Among the variables, two deserve attention because they are derived bases rather than values. spacingUnit is described as the base unit that all other spacing derives from — turn it up and the whole component becomes more spacious. fontSizeBase sets the root size, and the other font size variables scale from it in rem.

Compare that with the usual approach, where a design system publishes --space-1 through --space-12 as twelve independent hard-coded values. Both give you a scale. Only one gives you a dial. If a customer needs a denser form, the derived version is a single number; the enumerated version is twelve edits and a judgement call on each.

/* Enumerated: twelve values, twelve things to change */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
/* ... */

/* Derived: one dial */
--space-unit: 4px;
--space-1: calc(var(--space-unit) * 1);
--space-2: calc(var(--space-unit) * 2);
--space-3: calc(var(--space-unit) * 3);

This is worth applying beyond spacing. Any scale where the steps are genuinely proportional — spacing, type, radius — is better expressed as a base plus a ratio than as a list. Scales where the steps are not proportional, like a neutral colour ramp, are not: those need each step chosen by eye.

The documented exceptions are the honest part

The Appearance API documentation states that colorPrimary, colorBackground, colorText, colorSuccess, colorDanger and colorWarning do not support rgba() or var(--myVariable) syntax, while other variables do. It also notes that the API does not apply to individual payment-method Elements like CardElement, which use a separate Style object.

Those are unglamorous inconsistencies, and publishing them is the right call. A customisation API that quietly fails on a subset of inputs costs an integrator an afternoon of confused debugging; one that says so in the table costs them thirty seconds. If your own system has a rule that only works in some places, the documentation is where that belongs, not the changelog.

Applying both patterns to an agent-facing system

Both of these systems were designed for human developers reading documentation. The interesting question in 2026 is what changes when the developer is a coding agent, and the answer is that the Stripe Apps posture gets stronger while the Elements posture gets weaker.

An agent has no incentive to cut corners on a Friday, but it also has no memory of your conventions between sessions. Give it a closed vocabulary and it will use the vocabulary, reliably, forever. Give it a className prop and a paragraph saying "prefer the tokens" and it will use the tokens roughly as often as the training data does — which is to say, sometimes.

We sampled 299 DESIGN.md files published for AI agents to read. 86% specified colours as raw hex values with no semantic role attached, and 76% contained no prohibitions at all. Those two numbers describe a file that tells a model what colours exist and nothing about what they mean or what is forbidden — the exact opposite of both Stripe systems, which are almost entirely about meaning and constraint.

What the model does with it
Primary: #0570deUses it wherever blue seems reasonable — headings, links, borders, a gradient
--color-primary: #0570de — primary actions and active state only. Never for text, borders or backgrounds.Uses it on primary actions and active state. The prohibition is what makes the difference
Spacing: 4, 8, 12, 16, 24, 32Mostly uses these, occasionally emits 14px or 20px when the layout is tight
Spacing scale is xs/sm/md/lg/xl only. Any other value is a bug.Stays in the scale, because the file said what counts as wrong
The same guidance, written as a value list and written as a vocabulary.

That is the Stripe Apps idea moved into prose: you cannot give an agent a type error, but you can tell it what would be one. The Elements idea transfers too, as an ordering — state the theme-level defaults first, then the tokens, then the specific exceptions, so a model reading top to bottom encounters the general rule before the special case.

Identity Forge design kits are built on exactly this shape: semantic roles rather than raw hex, an explicit do's and don'ts list, and motifs that say what the design does rather than what values it contains. Browse the kits, or start with what a DESIGN.md file is.

What to take from this

Most teams building a design system pick one posture and apply it everywhere. Stripe's documentation is a demonstration that the posture should follow the boundary. On surfaces you are accountable for, close the vocabulary and make violations impossible. On surfaces someone else is accountable for, publish a ladder and let them climb only as far as they need.

Most products have both kinds of surface. The internal admin tool and the embeddable widget are not the same design problem, and a single system with one customisation story will serve one of them badly.

Can I download Stripe's design system?

Not the internal one used for the Dashboard and marketing site — it has never been published as a package or documentation site. The Stripe Apps UI toolkit and the Elements Appearance API are both public and documented, but they are systems for integrating with Stripe, not for building your own product.

What is the difference between Stripe Elements and the Stripe Apps UI toolkit?

Elements is Stripe's payment UI that you embed in your site, styled to match your brand through the Appearance API. The Stripe Apps UI toolkit is a component library for building apps that render inside the Stripe Dashboard, styled to match Stripe's brand with no way to override it. Different surfaces, different owner of the look.

Can I use arbitrary CSS in a Stripe App?

No. The css prop on Box accepts named tokens rather than free values, several components carry presets that cannot be overridden, and component hierarchy constraints restrict what can contain what. That is deliberate — it keeps thousands of third-party apps from making the Dashboard look inconsistent.

Should I customise Stripe Elements with variables or rules?

Variables first, always. They propagate across the whole Element and stay correct when Stripe updates the internals. Drop to rules only for something variables genuinely cannot express, because every rule you write is a selector you now own against future markup changes.

What does spacingUnit actually change in Stripe Elements?

It is the base value all other spacing in the Element derives from, so raising or lowering it makes the whole component uniformly more or less spacious without touching anything else. It is a single dial rather than a list of hard-coded gaps, which is the pattern worth copying into your own token layer.