Components
Typography
A small type system on the theme's tokens. Heading separates the semantic level from the visual size, Text covers body copy in sizes, tones and weights, and Prose styles raw HTML or MDX with no plugin.
Shipping a design system people actually use
Components are the easy part. Adoption comes from docs, defaults and a migration path that respects the code teams already have.
We started by auditing every button in the product and found 41 variations. Six months later there are three, all imported from @/components/ui/button, and nobody had to stop shipping to get there.
Installation
pnpm dlx shadcn@latest add @desyne/typographyThe CLI installs dependencies and any other components this one uses.
Usage
import {
Blockquote,
Code,
Heading,
Lead,
List,
Prose,
Small,
Text,
} from "@/components/ui/typography";<Heading level={1}>Billing</Heading>
<Lead>Plans, invoices and payment methods for your workspace.</Lead>
<Text tone="muted">Changes apply at the start of the next billing cycle.</Text>None of these are client components, so they render in server components with no JavaScript.
When to use
Heading— every title in an app screen or page. Picklevelfor the document outline andsizefor the look.Text,Lead,Small,Code— body copy you write in JSX, where you want the scale and tones without repeating class strings.Prose— content you don't write as JSX: MDX, Markdown rendered to HTML, CMS rich text, release notes from an API. It styles plain<h2>,<p>,<ul>and friends.- Inside components such as Card or Dialog, use their own title and description parts; they already follow this scale.
Anatomy
| Part | Renders | Notes |
|---|---|---|
Heading | <h1>–<h6> | level picks the element (default 2). size picks the style; without it each level has a default size. |
Text | <p> | as switches to span, div, strong, em or small. Sizes xs–lg, six tones, four weights. |
Lead | <p> | Larger, muted intro paragraph under a page title. |
Small | <small> | Fine print: legal lines, timestamps, footnotes. |
Code | <code> | Inline code. Sized in em, so it matches the surrounding text. |
Blockquote | <blockquote> | With attribution, wrapped in <figure> with a <figcaption>. |
List | <ul> / <ol> | ordered for numbers. Put <li> elements inside. |
Prose | <div> | Styles the HTML elements inside it with descendant selectors. |
The scale
| Size | Font size / line height | Default for | Typical use |
|---|---|---|---|
display | 36px → 48px from sm, 1.05 | Hero titles, big numbers | |
xl | 30px → 36px from sm, 1.25 | h1 | Page titles |
lg | 24px, 1.375 | h2 | Section titles |
md | 20px, 1.375 | h3 | Card and subsection titles |
sm | 18px, 1.375 | h4 | Small groups, sidebars |
xs | 16px, 1.5 | h5, h6 | Inline headings, list titles |
Body text runs xs 12/20, sm 14/24, md 16/28 (default) and lg 18/32. Headings are font-semibold with tighter tracking from md up; all sizes are standard Tailwind steps, so they follow your theme's --text-* values if you've changed them.
Quarterly planning
Hero titles, marketingQuarterly planning
Page titles (h1)Quarterly planning
Section titles (h2)Quarterly planning
Card and subsection titles (h3)Quarterly planning
Small groups (h4)Quarterly planning
Inline headings (h5, h6)Examples
Level and size
The outline and the look are separate decisions. A settings card might hold an h2 that should look small; a stat might be an h3 that should look huge. Set level for screen readers and the page outline, size for the eye. Don't skip levels to get a smaller heading.
Billing
Rendered as <h2> with the sm size, so the card stays compact without skipping a level in the outline.
99.98%
Rendered as <h3> with the display size: a big number that's still a small heading.
Text sizes and tones
tone sets the color from the theme: default for primary reading, subtle for softer body copy, muted for captions and helper text, plus brand, danger and success. default, subtle and muted all meet WCAG AA contrast for body text in light and dark mode.
Sizes
lg — Intro copy and roomy layouts.
md — The default for reading.
sm — Dense UI, cards and tables.
xs — Captions and footnotes.
Tones
default — Primary reading color.
subtle — Softer body copy.
muted — Metadata and helper text.
brand — Emphasis that matches links.
danger — Something went wrong.
success — Saved and synced.
Inline elements
Code for identifiers, file names and commands; Small for fine print; Text as="strong" or as="span" for emphasis inside other text.
Run npx shadcn@latest add @desyne/typography, then import Heading and Text wherever you'd reach for a raw <h2> or <p>. Press ⌘K to search the docs.
Prices exclude VAT. Annual plans save 20%.
© 2026 Wrenly Inc. Wrenly is a registered trademark. Terms apply.Lists and quotes
List keeps native list semantics and uses muted markers. marker="none" removes the bullets but keeps it announced as a list. Blockquote with attribution becomes a <figure> with a caption, the recommended markup for a quote with a source.
Before you launch
- Point your domain at the new deploy.
- Invite your team and assign roles.
- Turn on two-factor authentication.
Included in Pro
- Unlimited projects
- Audit log with 1-year retention
- SAML single sign-on
We replaced three internal libraries with one. New hires ship their first screen in a day instead of a week.
Prose
Wrap rendered HTML or MDX in Prose. It styles headings (h1–h4), paragraphs, links, bold text, lists (nested too), inline code, code blocks, blockquotes, horizontal rules, tables, images, figures and kbd. The first and last child lose their outer margin so the article sits flush in its container. Cap the line length with max-w-prose (65 characters).
Migrating to the new billing API
The v2 billing API replaces per-seat line items with usage records. Existing subscriptions keep working until June 30, but new features only ship on v2.
Timeline
- Now: v2 is available behind the
billing_v2flag. - April 15: new workspaces default to v2.
- June 30: v1 endpoints return
410 Gone.
Updating your client
Swap the subscription item call for a usage record:
await billing.usage.create({
subscription: "sub_42",
quantity: seats.length,
timestamp: Date.now(),
});Usage records are idempotent per timestamp, so retries never double charge.
Field changes
| v1 field | v2 field | Notes |
|---|---|---|
seats | quantity | Now reported, not set. |
plan | price | Prices are versioned. |
- Webhooks keep the same signatures.
- Invoices show usage per day.
- Totals are unchanged.
Questions? Reply to the migration email or open a ticket from Settings → Support.
Prose sizes
size sets the base font size; every margin and heading inside is in em, so the whole article scales together. Use sm in side panels and comments, lg for long-form reading.
Release notes · 4.2
Comments now support threads and @mentions. Everything else scales with the size you pick, because the article measures in em.
- Resolve a thread to collapse it.
- Mentions notify by email after five minutes.
With MDX
Pass Prose as the wrapper for your MDX content:
import { Prose } from "@/components/ui/typography";
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const { default: Content, title } = await import(`@/content/${slug}.mdx`);
return (
<Prose className="mx-auto max-w-prose">
<Content />
</Prose>
);
}For HTML from a CMS, sanitize it first and render it with dangerouslySetInnerHTML on the Prose element. To drop a styled component into an article (a Card, a chart) without the article styles reaching it, close Prose before it and open a new one after.
Guidance
- One
h1per page, thenh2for sections andh3inside them. Usesizeto adjust looks, never the level. - Keep lines short. 45–75 characters reads best:
max-w-proseon articles,max-w-xlormax-w-2xlon intros. - Use tone for hierarchy before size. A muted line under a title usually reads better than a smaller font.
- Don't put long text in color.
brand,dangerandsuccessare for short emphasis; pair them with an icon or wording so color isn't the only cue. - Balance and pretty. Headings use
text-wrap: balanceand body texttext-wrap: prettyto avoid lonely last words. - Numbers line up with
tabular-numsin tables and stats.
Accessibility
Headingalways renders a real heading element, so screen reader users can jump between headings and the outline matches what they see.Listkeeps list semantics; withmarker="none"it addsrole="list"because Safari otherwise stops announcing unstyled lists.Blockquotewithattributionuses<figure>and<figcaption>so the source is associated with the quote.- Links inside
Proseare underlined, not only colored. - Text sizes are in
remandem, so they follow the user's browser font size setting.
Styling
Data slots
| Slot | Element |
|---|---|
data-slot="heading" | Heading |
data-slot="text" | Text |
data-slot="lead" | Lead |
data-slot="small" | Small |
data-slot="code" | Code |
data-slot="blockquote" | Blockquote (the <figure> is blockquote-figure) |
data-slot="list" | List |
data-slot="prose" | Prose |
Reusing the styles
headingVariants, textVariants and proseVariants are exported, so you can apply the scale to other elements:
import { headingVariants, textVariants } from "@/components/ui/typography";
<Link className={headingVariants({ size: "sm" })} href="/changelog">Changelog</Link>
<label className={textVariants({ size: "sm", tone: "muted" })}>Optional</label>To change a Prose rule, edit the matching line in proseVariants (they're grouped by element), or add a descendant class at the call site: <Prose className="[&_a]:no-underline">.
API Reference
Heading
Prop
Type
Text
Prop
Type
Blockquote
Prop
Type
List
Prop
Type
Prose
Prop
Type
Lead, Small and Code take the native props of <p>, <small> and <code>.