Why foundations
Foundations pulls our design patterns and typography into one layer that installs with our React apps. Sane defaults, applied the same way in every repo, so whatever we ship starts from the same baseline.
Primitives store decisions
Three roles every page needs constantly: secondary copy, a timestamp, a section label. Each primitive renders markup you could have written by hand. The payoff comes at the next change to secondary ink, which is then a one-line diff in the package instead of a find-and-replace across your app.
A utility class is a fine way to express a style but a poor way to store a decision.
Spelled as text-sm text-muted-foreground instead, the decision sits at the call site, and a year in you have a few hundred call sites drifting a step apart. Two rules keep that from starting. When you are about to hand-write a type style, look for the primitive first. When you want to retune, reach for the CSS variables: the --text-* scale, --heading-weight, the colour tokens. The package owns its final classnames and reserves the right to change them.
Two commands set up the CSS
npx foundations initwrites the CSS block and reorders the imports you already have.npx foundations doctorreads your CSS entry, your root layout and your installed tree, then exits non-zero on a problem, so it can sit in CI next to your tests.
Three things have to be right before a component renders the way it should: the CSS imports in cascade order, the @source line that sends Tailwind into node_modules, and fonts bound with .variable instead of .className. If any of them are off, the build still passes and the types still look fine, which is why the doctor check is the one that catches it. The install section has both commands in context.
Tokens are named for meaning, not colour
The token is --success, never --green. Render success in blue and the name still reads true; move the brand and the component layer keeps its vocabulary. The package defines the structural roles only: background, foreground, card, muted, primary, border, ring, and the status set. Your brand colours stay in your app. Install them by overriding the raw variables after the imports, leaving the utilities alone.
.editorial came out of the same thinking. A heading size is a ratio to the body text underneath it, so the same TypographyH2 renders at 18px on a dense product screen and 30px in an essay to say the same thing. .editorial retunes the whole ladder and hands the heading role to the serif at weight 400; Average only ships that one weight. Put it on a div, a route group, or <html>. There is a switch for it in the header of this site if you want to see what it does to this page. The tokens page has the full palette.
Use the platform first
Disclosure is a <details> and a <summary>. No JavaScript, correct before hydration, keyboard-accessible because the browser handles that part. It works from a markdown file too, where you have no call site to pass props to, and it animates in CSS where the browser can. TabGroup uses Base UI, which leaves the keyboard handling, focus management and ARIA attributes to a team that maintains them full time.
Structure lives in CSS. A host framework can swap its own element in for yours and drop the classes on the way through. A child combinator in our stylesheet survives that.
One place for the rules
The design rules ship with the package, so every app enforces the same set from one line of config.
The rules are no-restricted-syntax selectors: plain data, no plugin, no ESLint dependency of their own. Spread designRules instead if you are on .eslintrc or composing the rule yourself.
The mistakes that pass review
Every rule in the set was a bug in shipped code first. An icon held into line by a negative margin that fitted one text size and missed every other. A hex in a gradient stop, one prefix past where the colour rule stopped looking. A paragraph set in text-sm text-muted-foreground beside forty others that had each drifted a step apart.
The types check. The page renders. A reviewer reads straight past: the line looks like every other line on the screen.
Lint is the only reader that catches this class of defect, so the rules travel with the components they protect.
- Colour. The raw palette (
bg-zinc-800), solid white and black, and a hex anywhere a token belongs, gradient stops included.inlineStyleextends that to a literal inside astyleobject, the one place a className rule cannot reach. - Type. An arbitrary font size in any unit. A rung passed as a class to a primitive that already owns a size prop. A type style hand-written on a
<p>or an<h2>, where the tag has already said the content is text. - Marks. A vertical margin nudging an icon into line with the words beside it. A glyph beside words goes in their
markprop. - Tokens. A deprecated
-foregroundspelling, adark:override that swaps one token for another, a surface token printed as ink. - Routing.
render={<a href>}on a component that takeshrefand would have picked the router, thereland the view transition together.
Four rules ship off by default. axis, pairing and leading each name a sweep you finish before switching them on. inlineStyle stays off for a different reason: a card rendered by Satori has to state its colours literally, since next/og resolves no custom properties.
Add a rule here and it reaches every app on the next version bump. The review comment gets written once.
Contrast is arithmetic, so we do it
Contrast ships as functions for the same reason the rules do. checkLegibility checks every ink against every surface in both themes using a 4.5:1 floor, resolving the cascade the way a browser would. A bare :root override of --background ties with .dark on specificity and wins in both themes at once, so a .dark block that measures 15.7:1 on its own can still render white on white.
Measure the two blocks separately and you have measured an intention.
A ratio has a second blind spot: it treats both directions the same, even though dark text on a bright field looks thinner and light text on a dark field often looks heavier. That is why the package also computes Lc, the APCA measure WCAG 3 is built on, which uses two exponent pairs, one per polarity.
Six weeks after the migration the queue drained on its own, which nobody had predicted and nobody could explain.
Six weeks after the migration the queue drained on its own, which nobody had predicted and nobody could explain.
Light
steps of 19 and 20 Lc
Body
ratio 15.25:1 · Lc 98.2Secondary
ratio 6.63:1 · Lc 79.6Tertiary
ratio 3.44:1 · Lc 59.5
Dark
steps of 18 and 30 Lc
Body
ratio 15.91:1 · Lc 95.2Secondary
ratio 12.10:1 · Lc 77.3Tertiary
ratio 6.75:1 · Lc 47.8
Every figure in that panel is computed at build time from the stylesheet this site installs, by the same two functions your app gets. Ratios answer the audit; lc answers whether your secondary ink is quieter than your primary, and whether it stayed that way in dark. The tokens page has the arithmetic and the call sites.
The documentation is a build step
This site, this page included, is built with @supertype.ai/foundations. It installs @supertype.ai/foundations from a git tag the same way our production apps do, and its global.css and layout.tsx are the install snippets from the README, unchanged.
Recipes holds whole pages instead of single components, each one a complete file that imports from the package alone. The build fails if a recipe reaches for a local helper, which keeps every example runnable outside this repo.
Most of the code that ends up using this package gets written by an agent, so it also ships an llms.txt covering the public API, the rules, and the mistakes that produce no error. yarn build fails if an export is missing from it, and one line in your AGENTS.md is usually enough to keep an agent from hand-writing text-sm text-muted-foreground where a primitive exists. That is the agents page.
These are small decisions, made once and applied at the places where a codebase starts to drift. To try it, start at install. For a whole page ahead of a component list, go to recipes first.