Skip to content

About

Elegant typography, prose, essay, and building blocks. Built for Next.JS + Shadcn + Tailwind.

Topics

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

@supertype.ai/foundations

ci npm license

The foundations philosophy explains the reasoning in more detail, but the short version is simple: this is a reusable design system for typography primitives, content blocks, the long-form essay shell, token and theme CSS, and the build-time tooling that keeps the baseline consistent. That includes SEO, OG cards, lint rules, and contrast checks.

It is used in software graduating from the Supertype Incubator, so projects like Viably work operating system, Supertype.ai, and Sectors Financial Data Platform all use the MIT-licensed Foundations as their design system.

It is designed thoroughly around the principles of reusability, consistency, and maintainability, ensuring that all projects built with Foundations have a solid (albeit slightly opinionated) typographic and styling foundation while sticking to best practices and design guidelines consistent with Next.JS 15+ and Tailwind 4+.

yarn add @supertype.ai/foundations

Start here: Install, then Your first page.

Reference: Typography · Blocks · The essay shell · Build-time tooling · The CLI

Working on the package itself: Contributing, for local iteration against a consumer and for releasing.

Like the project? ⭐ Star it on GitHub

See it running

Check out: the documentation site, or alternatively run the example site locally:

yarn example:install   # once, to install Next, the peers and the package
yarn example           # then open http://localhost:3000

examples/site renders every component with the code next to it, and includes whole-page recipes you can copy into your project. It also includes the dark and .editorial switches.

Initialization and Diagnostics

This package includes a CLI that writes the CSS for you and checks the rest:

npx @supertype.ai/foundations init      # edits your CSS entry, prints the rest
npx @supertype.ai/foundations doctor    # checks this app against everything below
npx @supertype.ai/foundations upgrade   # after a version bump, moves source the release changed

init edits one file: the CSS entry that imports Tailwind. It adds any missing imports and reorders the existing ones if needed. Run it with --dry-run first to preview the patch. It also prints the font bindings and the llms.txt snippet your coding agent should read.

upgrade is for later: after bumping the Foundations package, it rewrites source written for an older version and lists what it leaves. Works only on a clean git tree.

The steps performed by init are listed below. See the CLI for the full list of checks and details.

1. Add the package

yarn add @supertype.ai/foundations
# or: npm install @supertype.ai/foundations

Peers are Tailwind 4+, React 19+, Next 15+, next-view-transitions 0.3+ and @base-ui/react 1.4+.

tokens.css declares @custom-variant and @theme inline, and the @source line below is Tailwind-v4 syntax; on v3 they are parse errors. Run npx @tailwindcss/upgrade first to upgrade your Tailwind to v4. foundations init will warn you when you are using an out-of-date Tailwind version.

Installing from a git tag instead

Every release is tagged and published, so a commit can be installed directly as well.

// package.json
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.4.4"

2. Import the CSS

/* app/globals.css */
@import "tailwindcss";
@import "@supertype.ai/foundations";

That one line carries tokens.css, theme.css, type.css and prose.css in the order the cascade needs, and wires it up to Tailwind v4 resolution correctly.

Add @import "@supertype.ai/foundations/shiki.css"; after the two above if you render code fences.

Importing the parts separately

You can also import these CSS files separately. This is useful if you want to set your own colour values and leave out theme.css. In that case, use the import order below and add the @source path so Tailwind scans the package:

/* app/globals.css */
@import "tailwindcss";
@import "@supertype.ai/foundations/tokens.css"; /* structural tokens + dark variant */
@import "@supertype.ai/foundations/theme.css"; /* the house palette */
@import "@supertype.ai/foundations/type.css"; /* the type ramp + font roles */
@import "@supertype.ai/foundations/prose.css"; /* inline-code rule */

@source '../node_modules/@supertype.ai/foundations/dist/**/*.js';

The @source line is required in this form. Tailwind does not scan node_modules by default, so without it the package’s classes get purged and the components render without styles. The path is relative to your CSS file, so it changes with your layout — and in a workspace, where the package hoists to the repo root, ../node_modules is not where it lives.

theme.css is required. tokens.css names the colour roles, and theme.css gives them values. Without it, the colour utilities cannot be resolved, so the page renders unpainted without an obvious error. It also carries --secondary-ink, --subtle-foreground and the four earth tones used for marker highlights. Skip it only if you declare every role yourself; foundations doctor fails if neither path is true.

3. Bind the fonts

The package cannot load the typefaces for you. next/font runs in your app and generates hashed variable names at build time, so each app loads the three fonts and binds them to the roles type.css expects:

// app/layout.tsx
import { Ubuntu_Sans, Ubuntu_Sans_Mono, Average } from "next/font/google";

const sans = Ubuntu_Sans({ variable: "--font-ubuntu-sans", subsets: ["latin"] });
const mono = Ubuntu_Sans_Mono({ variable: "--font-ubuntu-sans-mono", subsets: ["latin"] });
const serif = Average({ variable: "--font-average", weight: "400", subsets: ["latin"] });

<html className={`${sans.variable} ${mono.variable} ${serif.variable} font-sans`}>

Bind with .variable, never .className. A className sets font-family on the element itself and leaves the roles unresolved, which causes a mismatch where the page renders one typeface while the font-sans and font-heading utilities render another.

4. Check the wiring

npx foundations doctor

It reads your CSS entry, your root layout and the installed tree, then reports on import order, the @source path, the font bindings and the peer versions. It exits non-zero on a real problem, so it works as a CI step too. Every check and what it catches is listed in the CLI.


Your first page

import {
  TypographyH1,
  TypographyH2,
  TypographyProse,
  TypographyEyebrow,
  TypographyLink,
  TypographyCaption,
} from "@supertype.ai/foundations";
import { Card, Cards, Callout } from "@supertype.ai/foundations/blocks";

export default function Page() {
  return (
    <main className="mx-auto max-w-3xl px-6 py-16">
      <TypographyEyebrow>Guides</TypographyEyebrow>
      <TypographyH1 variant="display" className="mt-2 text-balance">
        Getting data out of Postgres
      </TypographyH1>
      <TypographyProse className="mt-4">
        Three approaches, ordered by how much of your schema they need to know.
      </TypographyProse>

      <TypographyH2 divider className="mt-12">
        Approaches
      </TypographyH2>

      <Cards>
        <Card
          href="/notes/logical-replication"
          title="Logical replication"
          description="Row-level changes, no schema coupling."
        />
        <Card
          href="https://www.postgresql.org/docs/current/sql-copy.html"
          title="COPY"
          description="Fastest bulk path. Leaves the app."
        />
      </Cards>

      <Callout tone="warn" title="Before you start" className="mt-8">
        Replication slots hold WAL until they are consumed. An abandoned slot
        fills the disk — see{" "}
        <TypographyLink href="/ops/slots" addArrow>
          slot hygiene
        </TypographyLink>
        .
      </Callout>

      <TypographyCaption as="p" className="mt-8">
        Last reviewed October 2026
      </TypographyCaption>
    </main>
  );
}

Two rules cover most of the API:

  • Do not write type styles by hand. A paragraph carrying text-sm text-muted-foreground is <TypographyMuted>. Using the primitives keeps a size and a colour from drifting apart across the entire codebase.
  • Retune with CSS variables, not classes. The package owns its own classnames. Change a --text-*, --heading-weight, or a colour specification in theme.css to retune the whole package. Read Tokens and theming for instructions.

The example site

yarn example (above) builds the package, syncs it in and starts the dev server. yarn example:build is what CI would run.

It installs the package from a git tag rather than the registry and updates it with yarn sync.

/recipes holds whole pages rather than single components: a marketing hero, a metrics panel, pricing tiers, a docs page, an article index, and examples of MDX-rendered pages. Each one lives in app/_recipes/ as a complete file that imports only from this package, so you can paste it into your app.


For coding agents

The package includes an llms.txt with the public API, rules and usage guidance. Point your agent at it once and it stops writing text-sm text-muted-foreground where a primitive exists:

<!-- CLAUDE.md, AGENTS.md, or your agent's equivalent -->

@node_modules/@supertype.ai/foundations/llms.txt

Entry points

import contains docs
@supertype.ai/foundations all typography primitives, cn Typography
@supertype.ai/foundations/blocks Button, Badge, Card, Callout, Steps, TabGroup, Disclosure, SEGMENT Blocks
@supertype.ai/foundations/mdx proseMdxComponents — the MDX element map In MDX
@supertype.ai/foundations/essay the long-form shell, TOC, reading rail, post meta Essay
@supertype.ai/foundations/seo createSeo(...) — metadata + JSON-LD Tooling
@supertype.ai/foundations/og ogCard, OG_SIZE — an element for next/og Tooling
@supertype.ai/foundations/eslint the design rules as ESLint selectors; five ship off until an app has swept for them Tooling
@supertype.ai/foundations/rehype rehypeProseCode — build-time only In MDX
@supertype.ai/foundations/contrast token resolution + legibility checks, build-time only Tooling
./tokens.css ./theme.css ./type.css ./prose.css ./shiki.css the style layer Tokens and theming
foundations (bin) init and doctor The CLI

Tokens and theming

tokens.css names the structural roles: --background, --foreground, --card, --muted, --primary, --border and --ring, plus the status set. They are named for meaning rather than hue, so a project that renders success in blue still reads correctly. It holds no values, only the roles themselves.

Each status hue ships twice, on the same rule as the categorical tints: --success, --warn and --info are fills, held to 3:1 against the page and a card; --success-ink, --warn-ink and --info-ink are the same hues as text, held to 4.5:1. --danger ships as an ink only. --destructive keeps shadcn's shape, where --destructive-foreground is the label printed on the fill — that is what -foreground means throughout, and -ink means the hue used as words. checkSignals in @supertype.ai/foundations/contrast measures all of them and fails if any are below the threshold.

theme.css gives those roles the latte and espresso palette, and adds the editorial inks (--secondary-ink, --subtle-foreground, and the ochre, terracotta, sage and fig pairs) along with the elevation shadows.

tokens.css also binds the dark: variant to the .dark class which sets color-scheme: dark such that scrollbars, form controls and autofill follow the palette, including on a .dark section of a light page. Each theme's ground is available by name on :root as --latte-background and --espresso-background. Use them when something outside a .dark element needs the dark ground. The usual case is <html> behind a page that pins its content dark, which otherwise shows the light ground on overscroll:

html:has(.dark[data-ground]) {
  background-color: var(--espresso-background);
}

No brand colours in the package. Structural tokens only, with brand colours left to the app. To repaint, override the raw variables after the imports rather than patching the utilities:

:root {
  --primary: hsl(24 60% 42%);
}
.dark {
  --primary: hsl(24 70% 62%);
}

.editorial

type.css names three font roles (--font-sans, --font-mono and --font-heading) and the weight that goes with the heading face. .editorial gives the heading role to the serif and drops the weight to 400.

<div className="editorial">…</div>   {/* or on <html> for an editorial site */}

Heading sizes are a ratio to the body text under them with 13px in the product, 18px on .editorial. Scope the class to whichever surfaces should be editorial, whether that is a marketing and docs section or the whole site.


Design rules

  1. The package owns its final classnames. Retune with CSS custom properties (the --text-*, --heading-weight, the colour tokens) rather than by patching classes. A property the package declares is read by the package; if a knob turns nothing, test/tokens-live.test.ts fails on it.
  2. No variant props on the MDX map. Elements that MDX renders automatically take no options.
  3. Native html first, and a library only where it falls short. Disclosure is a <details>/<summary> pair: no JavaScript, correct before hydration, available to an MDX author, and animated by CSS where the browser can. TabGroup uses Base UI because managed selection is beyond what native HTML offers.
  4. No brand colours. Structural tokens only, with brand colours left to the app.
  5. Put structure in CSS rather than the component map. A host framework can substitute its own element and strip the classes off it, but it cannot strip a child combinator. Both the Shiki theming and the inline-code rule rely on this.

Upgrading to 0.4

With 0.4, there is one way to put an icon, checkbox or dot beside words.

// before
<TypographyCaption className="flex items-center gap-1">
  <Icons.Clock className="size-3" /> 44d ago
</TypographyCaption>

// after
<TypographyCaption mark={<Icons.Clock />}>44d ago</TypographyCaption>

Removed

Every removal corresponds to a feature that 0.4 replaces with the new mark and markEnd system.

0.3 0.4
ON_FIRST_LINE wrapper beside a block of text the words' mark prop, or <Marked mark title> for a title with lines under it
ON_BASELINE on a row items-baseline for words at two sizes; an icon in the row goes in the words' mark or a <Mark>
icon-inline utility nothing: a glyph in a mark is one em of the words
TypographyCaption sizing a direct-child svg pass the icon as mark or markEnd; a loose one renders at Lucide's 24px
CAP_TRIM on a label so items-center meets an icon the label's mark; CAP_TRIM stays for text centred in its own box or beside an object taller than the line
inline + align-middle or -mt-* on an icon <Mark> around it, or the words' mark
TypographySmall TypographyCaption: as="small" for small print, as="p" for a note. upgrade writes as="p", which keeps the block the old one rendered
TypographyProseList TypographyList variant="prose"
Accordion, AccordionItem, AccordionTrigger, AccordionContent DisclosureGroup and Disclosure, which now animate open and closed. A literal defaultValue becomes open on those rows; state the app keeps is open and onToggle on each Disclosure
Tabs, TabsList, TabsTrigger, TabsContent TabGroup, built with .map when the tabs are data. A strip with no panels is a TabGroup picker (from 0.4.1); a compact one is size="sm"; a frame around the strip and panels goes around the TabGroup
DISCLOSURE Disclosure; the surfaces are its own
INK_ON_CARD, INK_ON_POPOVER, INK_ON_SIDEBAR style={inkOnSurfaceStyle("--card-foreground")}, and the same for any surface
the accordion-down and accordion-up keyframes nothing: Disclosure animates ::details-content

New

  • mark and markEnd on every text role, headings and CardTitle included. A leading mark starts the line; an end mark follows the words, and justify-between sends it to the edge.
  • <Marked mark={…} title={<TypographyLabel>…</TypographyLabel>}>lines</Marked> for a title and the lines under it.
  • <Mark>, the slot on its own, for a glyph inside a sentence or in words no role renders.
  • truncate on every text role. With a mark it cuts the words, never the mark.
  • From 0.4.1, TabGroup with no content on any tab is a picker, and size="sm" is the compact strip for a toolbar or a chart.
  • From 0.4.1, no block gives itself a margin. Cards, Steps, DisclosureGroup and TabGroup dropped their my-6: in prose ProseFlow spaces them, and elsewhere the layout holding them does, as it already did for every other block. One that sat bare in a page gets its space from a gap or a space-y-* on its container.

Changed

  • A glyph beside words is one em of them. Meta icons in a caption grow from 0.8em to 1em; an icon that carried a bigger size class shrinks to 1em.
  • Button, Badge and a TabGroup tab size a glyph to one em of the label, by the rule every mark uses: a bare glyph, a direct child. Button used to reach a glyph at any depth; one you wrap in an element of your own now keeps the size you give it. Badge size="xs" no longer forces a 12px icon into a 10px label.
  • Badge trims its label as Button does, and both keep adjacent words as one label, so {count} to act on no longer splits around the gap.
  • TypographyCaption states its regular weight, so a count inside a label's line stays regular.
  • TypographyLink with no tone takes the ink around it. It defaulted to muted, which is the same ink on a page and failed contrast on a filled surface. Its addArrow glyph is a mark, one em of the link.
  • Disclosure animates open and closed where the browser supports ::details-content and interpolate-size, and opens at once elsewhere.
  • The design lint (designRules) flags a loose icon in a text role, a size class on a glyph in a mark, a <Mark> or a control, and a glyph held into line with a top margin or align-middle.

How to upgrade

upgrade does most of this. Take 0.4.1 or later, whose upgrade also handles the accordions, tabs and surface inks automatically.

  1. Bump the package and run npx foundations doctor. Its Source check counts the files still written for 0.3.
  2. On a clean git tree, run npx foundations upgrade --dry-run to see what moves, then npx foundations upgrade to move it. It reads your .tsx and .jsx, writes only the files it moves something in, and the diff is exactly what it did. It moves:
    • a glyph first or last inside a text role into mark/markEnd, a glyph beside a text role in a flex row into that role's mark, and an inline glyph nudged with align-middle or a top margin into <Mark>;
    • TypographySmall and TypographyProseList to the props they pinned;
    • an Accordion of trigger-and-content items to DisclosureGroup and Disclosure, a literal defaultValue included;
    • a Tabs strip, written out or built with .map, with or without panels, to TabGroup, its compact classes to size="sm" and a box around it to a box around the TabGroup;
    • an INK_ON_* in a className to style={inkOnSurfaceStyle(…)};
    • and the imports that go with each.
  3. Do what it marks ✖ by hand: each line names the file, the line and why it could not move the code (an accordion whose open state the app keeps, a strip beside other controls in one row, an attribute it has no place for). Look at what it marks !: those moved, and each says what to check, such as a glyph's colour or a panel's spacing.
  4. Run the compiler and your lint. Check what's left, e.g., an import of a removed name fails to resolve, and designRules flags a glyph that is not yet a mark.
  5. Look at the pages. No tool sees these: words that a flex gap used to separate and now need a {" "}, centred or right-aligned words, where an inline <Mark> keeps the glyph with them, and an avatar or anything else taller than the line (in the last case, Foundations use CAP_TRIM to align it).

Example Sites

Sites running the package:

  • supertype.ai — Supertype, a regional-leading analytics engineering and data science consulting firm.
  • viably.app — Viably, an observability-first business operating system and CRM for automation-obsessed teams.
  • sectors.app — Sectors, a financial data platform and market intelligence suite built for the Indonesia and Singapore markets.

License

MIT. Copyright © 2026 Supertype. See LICENSE.

Published to npm as @supertype.ai/foundations, and installable from this repository by tag.

About

Elegant typography, prose, essay, and building blocks. Built for Next.JS + Shadcn + Tailwind.

Topics

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages