Skip to the content

ComponentsMisc

Typography

The type scale: Prose styles Markdown or rich-text HTML; Heading, Text, Blockquote and InlineCode style your own elements.

Import

import { Blockquote, Heading, InlineCode, Prose, Text } from "@booleanpress/ui/typography"

Usage

Wrap the HTML a Markdown or rich-text renderer gives you in Prose: headings, paragraphs, lists, links, quotes, code, tables, rules and images take the scale without classes of their own.

import { Heading, Prose, Text } from "@booleanpress/ui/typography"

export function HelpArticle({ html }: { html: string }) {
  return (
    <>
      <Heading level={1}>Moving to a new mailer</Heading>
      <Text size="lg" tone="muted">What changes, and what stays the same.</Text>
      <Prose dangerouslySetInnerHTML={{ __html: html }} />
    </>
  )
}

The scale is the visual target's documentation, in Inter: h1 30/36 regular, h2 20/28, h3 18/28, h4 16/24, h5 14/21 and h6 12/18 medium, in the heading colour; running text in Prose is 16/24 and text in components 14/21. Inside Prose, paragraphs are 16 px apart, headings sit 4 px above their text with 56, 32 or 24 px above them, list items are 6 px apart, and links are underlined.

Heading renders h1 to h6 from level (2 by default); size draws it at another level's size, so the outline and the look can differ. Text renders a p (asChild for a span) with size xs, sm (default), base or lg, a tone and a weight. Blockquote and InlineCode match Prose's quotes and code. Prose styles every heading, list and link inside it, so keep other components beside it rather than inside.

Examples

Prose

An article: headings, paragraphs, a link, inline code, bold text, a code block and a rule.

import { Prose } from "@booleanpress/ui/typography"

export default function TypographyProse() {
  return (
    <Prose asChild className="max-w-xl">
      <article>
        <h1>Moving to a new mailer</h1>
        <p>
          Switching providers does not lose a single email. The old mailer keeps sending until the new one passes its
          first test, and the <a href="#delivery-log">delivery log</a> shows both side by side.
        </p>
        <h2>Before you start</h2>
        <p>
          Collect the new provider’s host, port and API key. Keys are shown once, so store yours in a secrets manager,
          not in <code>wp-config.php</code>.
        </p>
        <h3>Check your DNS</h3>
        <p>
          Add the provider’s <strong>SPF</strong> and <strong>DKIM</strong> records. Receiving servers check them on
          every message.
        </p>
        <pre>
          <code>{`v=spf1 include:_spf.example.com ~all`}</code>
        </pre>
        <h4>How long it takes</h4>
        <p>DNS changes usually reach every server within an hour, and always within two days.</p>
        <hr />
        <p>Questions? The support team answers within one working day.</p>
      </article>
    </Prose>
  )
}

Headings

Heading at each level, and an h2 drawn at size 4.

import { Heading } from "@booleanpress/ui/typography"

export default function TypographyHeadings() {
  return (
    <div className="flex flex-col gap-3">
      <Heading level={1}>Delivery log</Heading>
      <Heading level={2}>Failed deliveries</Heading>
      <Heading level={3}>Bounced by the receiving server</Heading>
      <Heading level={4}>Soft bounces</Heading>
      <Heading level={5}>Mailbox full</Heading>
      <Heading level={6}>Last checked 4 October 2026</Heading>
      <Heading level={2} size={4} className="text-muted-foreground">
        An h2 drawn at size 4
      </Heading>
    </div>
  )
}

Text sizes and tones

Text from the 18 px lead to 12 px small print, and its tones.

import { Text } from "@booleanpress/ui/typography"

export default function TypographyText() {
  return (
    <div className="flex max-w-md flex-col gap-3">
      <Text size="lg" tone="muted">
        A lead paragraph: what this page is for, in one sentence.
      </Text>
      <Text size="base">Running text, for help pages and articles: 16 pixels on a 24 pixel line.</Text>
      <Text>Component text, the default: 14 pixels on a 21 pixel line.</Text>
      <Text size="xs" tone="muted">
        Small print: last synced 5 October 2026.
      </Text>
      <div className="flex flex-wrap gap-x-4 gap-y-1">
        <Text tone="strong" weight="semibold">
          Strong
        </Text>
        <Text tone="muted">Muted</Text>
        <Text tone="success" weight="medium">
          Delivered
        </Text>
        <Text tone="destructive" weight="medium">
          Bounced
        </Text>
      </div>
    </div>
  )
}

Lists and quotes

Bulleted, nested and numbered lists and a quote inside Prose; Blockquote on its own.

import { Blockquote, Prose } from "@booleanpress/ui/typography"

export default function TypographyListsAndQuotes() {
  return (
    <div className="flex max-w-xl flex-col gap-6">
      <Prose>
        <ul>
          <li>Connect a mailer</li>
          <li>
            Verify your domain
            <ul>
              <li>Add the SPF record</li>
              <li>Add the DKIM record</li>
            </ul>
          </li>
          <li>Send a test email</li>
        </ul>
        <ol>
          <li>Create an API key.</li>
          <li>Paste it into the mailer’s settings.</li>
          <li>Save, then press Send test.</li>
        </ol>
        <blockquote>
          <p>We moved forty sites to the new mailer in an afternoon, and no customer noticed.</p>
        </blockquote>
      </Prose>
      <Blockquote>A Blockquote on its own, outside Prose, looks the same.</Blockquote>
    </div>
  )
}

Table inside prose

A plain table in Prose: full width, a rule under the header and each row.

import { Prose } from "@booleanpress/ui/typography"

const ROWS = [
  ["Delivered", "The receiving server accepted the message.", "1,284"],
  ["Deferred", "The server asked us to try again later.", "12"],
  ["Bounced", "The server refused the message for good.", "7"],
]

export default function TypographyTableInProse() {
  return (
    <Prose className="w-full max-w-xl">
      <h3>Delivery states</h3>
      <p>Every message in the log is in one of three states.</p>
      <table>
        <thead>
          <tr>
            <th>State</th>
            <th>Meaning</th>
            <th>Today</th>
          </tr>
        </thead>
        <tbody>
          {ROWS.map(([state, meaning, count]) => (
            <tr key={state}>
              <td>
                <code>{state.toLowerCase()}</code>
              </td>
              <td>{meaning}</td>
              <td>{count}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </Prose>
  )
}

Accessibility

Semantics
Every part renders the native element: Heading h1–h6, Text p, Blockquote blockquote, InlineCode code; Prose is a div (asChild for an article) that styles the elements inside it and changes none.
Labels
No names of their own: the text is the content.
Focus
Nothing takes focus but the links inside, which show the focus outline on keyboard focus.
Known limits
  • Choose level for the page's outline, not for the size: one h1 per page, no skipped levels. Use size for the look.
  • A table wider than the text overflows it: wrap a wide table in a scrolling element with tabIndex={0}, role="region" and a name.
  • A pre inside Prose scrolls sideways, but not every browser lets a keyboard focus it to scroll: use CodeBlock for long code.
  • Links inside Prose are always underlined, unlike the visual target's, so they are not told apart by colour alone.
  • The success tone is the status tags' deep green, not the brighter green of alerts, so it keeps 4.5:1 on the page in both themes; colour alone still says nothing to some readers, so let the words carry the meaning.

Keyboard

Keyboard
KeyBehaviour

API

Blockquote

Renders a blockquote and passes it every other prop.

Heading

Renders a h2 and passes it every other prop.

Heading props
PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead (a DialogTitle, a link), with the heading's classes merged onto it.
level1 | 2 | 3 | 4 | 5 | 62The heading's level in the page's outline: renders h1 to h6. 2 by default.
size1 | 2 | 3 | 4 | 5 | 6The size of another level, when the look should differ from the outline (an h2 drawn at size 3).

InlineCode

Renders a code and passes it every other prop.

Prose

Renders a div and passes it every other prop.

Prose props
PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead (an article), with the prose classes merged onto it.

Text

Renders a p and passes it every other prop.

Text props
PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead (a span, a label), with the text's classes merged onto it.
size"base" | "xs" | "sm" | "lg"smxs 12/18, sm 14/21 (default), base 16/24, lg 18/28.
tone"strong" | "default" | "destructive" | "success" | "muted"defaultThe colour: default, muted, strong (the heading colour), destructive or success.
weight"bold" | "medium" | "normal" | "semibold"normal, medium, semibold or bold; inherited when left out.

Also exported: headingVariants, the class names of Heading's variants and sizes (cva), to give another element the same look; textVariants, the class names of Text's variants and sizes (cva), to give another element the same look; HeadingLevel, a TypeScript type.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="blockquote" (Blockquote), data-slot="heading" (Heading), data-slot="inline-code" (InlineCode), data-slot="prose" (Prose), data-slot="text" (Text), and data-level, data-size, data-tone.

Theming

Headings and bold text use --heading, running text --foreground, muted text and quotes --muted-foreground, inline code --border behind --foreground, links --primary. Code uses font-mono: set --font-mono in your theme for another typeface.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--borderborder, background
--cardbackground
--destructive-strongtext
--foregroundtext
--headingtext
--muted-foregroundtext
--primarytext, underline
--ringoutline
--success-tag-foregroundtext