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.
<TypographyMuted>Only what the product needs to run.</TypographyMuted>
<TypographyCaption as="p">Last reviewed Sep 2026</TypographyCaption>
<TypographyEyebrow>Guides</TypographyEyebrow>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, because Average only ships the 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. Accordion and Tabs use 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.
// eslint.config.js
import { designConfig } from "@supertype.ai/foundations/eslint";
export default [
...designConfig({ accents: "the brand tints", weights: true }),
];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.
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.