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.
Every component outside typography and prose. They ship from two import subpaths, and each group names the one it comes from.
@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.
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.
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.Replication slots are consumed in order. See slot hygiene.
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.
| solid | soft | outline | ghost | |
|---|---|---|---|---|
| muted | Queued | Queued | Queued | Queued |
| primary | Queued | Queued | Queued | Queued |
| secondary | Queued | Queued | Queued | Queued |
| brand | Queued | Queued | Queued | Queued |
| success | Queued | Queued | Queued | Queued |
| warn | Queued | Queued | Queued | Queued |
| destructive | Queued | Queued | Queued | Queued |
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.
#main.npx foundations initto write the imports for you..variable, never .className.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.
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.
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.
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.
Anchors, with depth
Routes, via render
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.
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.
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}
</>
);
}@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.
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.
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.
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.
extractHeadings walks the raw markdown, so the index is ready before the MDX is compiled.
createSlugger follows the same algorithm as rehype-slug, so rail anchors match the ids on the headings.
ReadingRail takes the list as returned. The depth on each heading is what drives the indent.
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.
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 });@supertype.ai/foundations/essay
The functions that produce heading data for the rails, and the two hooks they run on.
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)
#the-queue#draining-it#the-measurecreateSlugger()
#the-queue#the-queue-1readingTime(source)
1 min. The fenced block is excluded from the count.
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>;
}