foundations
Star
Reference

Blocks

Every component outside typography and prose. They ship from two import subpaths, and each group names the one it comes from.

Content

@supertype.ai/foundations/blocks

Components that sit inside prose. Tabs and Accordion are client components built on Base UI. The rest render on the server.

Card

Pass title and description for the default layout, or compose the slots directly for full control. An href makes the whole card a link; an href with a scheme opens in a new tab.

Callout

Two variants. compact is sized to sit inside a panel or sidebar. editorial sets the body at reading size and adds an accent rail, for docs and long-form pages.

Rotate the key first

The old one stops working the moment the new one is issued.

This deletes the slot

An abandoned replication slot holds WAL until the disk fills.
Why this is safe

Replication slots are consumed in order. See slot hygiene.

With no tone set, a callout renders muted.

Button

Two independent axes. variant is how much ink the button spends, and tone is what the ink means. A tone sets three custom properties, a variant spends them, and the grid below comes to thirty-five pairs from twelve declarations. The seven tones belong to the package rather than to this component: Callout and TypographyLink take the same list.

solidsoftoutlineghostlink
muted
primary
secondary
brand
success
warn
destructive

Badge

The same two axes as Button, spelled the same way, minus link, which belongs to things you click. Both apps had grown a private list instead, one carrying warning and supertype for tones the package already named warn and brand.

solidsoftoutlineghost
mutedQueuedQueuedQueuedQueued
primaryQueuedQueuedQueuedQueued
secondaryQueuedQueuedQueuedQueued
brandQueuedQueuedQueuedQueued
successQueuedQueuedQueuedQueued
warnQueuedQueuedQueuedQueued
destructiveQueuedQueuedQueuedQueued
73A pill

Steps

A numbered sequence. Numbers come from a CSS counter, so reordering steps renumbers them automatically, and the digits are excluded from copied text and from the accessibility tree.

Add the package
Pin a tag, never #main.
Import the CSS, in order
Or run npx foundations initto write the imports for you.
Bind the fonts
With .variable, never .className.

Disclosure

Expandable content built on the native <details> element: no JavaScript, correct before hydration. Set a shared name to make a group single-open, which the browser handles for you.

Retries
Three attempts, then backoff.
Timeouts
30s, then the job is requeued.
Dead letters
Kept 14 days, then dropped.

Accordion

Use Accordion for animated transitions or controlled selection, and Disclosure everywhere else, since it is lighter. The open and close keyframes live in theme.css; skip that import and panels snap open.

Tabs

TabGroup takes the tabs as data, which is the shape to reach for. Its variant picks the box: default renders a boxed segmented track, line drops the surface and underlines the active tab, as in the preview and code switcher on this page. Its tone inks the marker alone, which on line means the underline and the active tab's icon.

npx foundations doctor
The active icon takes the list's tone.

Rail and RailLink

The primitive behind every index in the package: a list with a rule down its side. On RailLink, active highlights an item, nested indents a sub-heading, and render swaps the anchor for a router link. It runs without client hooks, so it works in a server component.

TableOfContents

A margin index that highlights the current section with a scroll spy. Pass sections as { id, label } matching the ids on your headings. label renames the rail heading; pass null to hide it. An empty list renders nothing at all. The examples below spy on this page, so they update as you scroll.

ReadingRail

TableOfContents plus a progress donut, for pages read start to finish. Takes TocHeading[], { depth, id, label }, the shape extractHeadings returns. Headings at depth 3 are indented.

ReadingProgressBar

A hairline progress indicator fixed to the top of the viewport, for the widths where the rail is hidden. Mount one per page, above your header. It shares a single scroll listener with ReadingRail, so using both costs one subscription. The preview is omitted here, since it would track this page instead of the example.

import { ReadingProgressBar } from "@supertype.ai/foundations/essay";

export default function ArticleLayout({ children }) {
  return (
    <>
      {/* Fixed to the viewport. One per page, above your header. */}
      <ReadingProgressBar />
      {children}
    </>
  );
}

Editorial

@supertype.ai/foundations/essay

Layout, meta, and the shell that composes them. Use the individual pieces for MDX articles, and EssayDocument when the content is data.

EssayColumns

A three-track grid: margin, measure, and an aside column that often sits empty. Reserving that third track holds the prose on one axis across pages with and without asides. The measure widens per step, and the aside appears once the container reaches 72rem. That is a container query rather than a viewport one, so a shell mounted in a narrow column drops the rail and centres the prose instead of drawing a margin too thin to hold a label.

Reach for EssayColumns directly when the margin holds something other than a rail. For an article, ReadingLayout composes these columns with the reading rail already in the aside.

ReadingLayout

EssayColumns with the reading rail already in its margin, for an article whose body the app supplies whole, as prose or compiled MDX. It is EssayLayout's counterpart: the same column, the same sticky offset, with the scroll-spied rail in place of the declared-section index. A piece with no headings drops the rail, so a short post gets a centred measure in place of a margin holding an empty nav. The body opens with EssayBody, the one piece that rules the seam under a header, so the narrow layout gets that line for free.

Reach for ReadingLayout when the body arrives as prose or MDX. Pass the headings you pulled out of the markdown, and the rail, its sticky offset and the headingless case are handled for you.

EssayMovements

An ordered list for stages that feed into one another: a pipeline, a method, a sequence of phases. Reach for it over a card grid when the order carries meaning.

  1. 01

    Read the source

    extractHeadings walks the raw markdown, so the index is ready before the MDX is compiled.

  2. 02

    Slug every heading

    createSlugger follows the same algorithm as rehype-slug, so rail anchors match the ids on the headings.

  3. 03

    Hand them to the rail

    ReadingRail takes the list as returned. The depth on each heading is what drives the indent.

Post meta

The byline row under an article title. PostMetaRow lays out PostDate, ReadTime and TagPills, separated by MetaDot. Dates format in en-US on both server and client, which keeps hydration free of a locale mismatch. Use formatPostDate for the same output outside React, such as in an OG image or a feed.

7 min readinfrastructurepostgres
7 min read

The essay shell

EssayHeader, EssayLayout, EssaySection, EssayPullQuote and EssayFigure compose an article by hand. EssayDocument takes the sections as data instead and builds the margin index from them. See a rendered essay for what they look like at full page width.

import {
  EssayHeader,
  EssayLayout,
  EssaySection,
  EssayPullQuote,
  EssayFigure,
  EssayDocument,
  createEssay,
} from "@supertype.ai/foundations/essay";

// Sections as data. The margin index is derived from them, so renaming a
// heading updates its rail link automatically.
<EssayDocument
  eyebrow="Engineering"
  title="What we learned shipping to forty workspaces"
  sections={[{ heading: "The queue", body: "Three months, one migration." }]}
/>;

// createEssay binds your own motion and decoration components once. Call it at
// module scope: each call defines new component types, and calling it during a
// render would remount the tree on every pass.
const { EssayHeader: Decorated } = createEssay({ Reveal, Glow });

Helpers

@supertype.ai/foundations/essay

The functions that produce heading data for the rails, and the two hooks they run on.

Headings from markdown

extractHeadings pulls h2 and h3 from raw markdown, ignoring fenced code. It runs on the string alone, so it works at build time. Ids come from createSlugger, which follows GitHub's algorithm and therefore matches the ids rehype-slug adds to your headings. readingTime skips code blocks, which keeps the estimate honest on technical posts. The output below was generated at build time.

extractHeadings(source)

  • h2 The queue #the-queue
  • h3 Draining it #draining-it
  • h2 The measure #the-measure

createSlugger()

  • The queue: #the-queue
  • The Queue!: #the-queue-1

readingTime(source)

1 min. The fenced block is excluded from the count.

Scroll hooks

The hooks behind TableOfContents and ReadingRail, exported so you can build your own. useScrollSpy tracks a list of heading ids with a single IntersectionObserver. useReadingProgress returns 0 to 1 from a shared store, so any number of callers on a page share one scroll listener.

"use client";

import {
  useScrollSpy,
  useReadingProgress,
} from "@supertype.ai/foundations/essay";

export function MyRail({ ids }: { ids: string[] }) {
  // Returns the id of the topmost heading on screen. The hook keys on the
  // joined ids, so building the array inline each render is fine.
  const active = useScrollSpy(ids);

  // Returns 0 to 1. All callers on a page share a single scroll listener.
  const progress = useReadingProgress();

  return <p>{active} · {Math.round(progress * 100)}%</p>;
}