# 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"`
- **Page:** <https://ui.booleanpress.com/components/typography> · @booleanpress/ui 0.2.0

## 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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

| Key | Behaviour |
| --- | --- |

## API

### Blockquote

Renders a `blockquote` and passes it every other prop.

### Heading

Renders a `h2` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead (a `DialogTitle`, a link), with the heading's classes merged onto it. |
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | The heading's level in the page's outline: renders `h1` to `h6`. 2 by default. |
| `size` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` |  | The 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead (an `article`), with the prose classes merged onto it. |

### Text

Renders a `p` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead (a `span`, a `label`), with the text's classes merged onto it. |
| `size` | `"base" \| "xs" \| "sm" \| "lg"` | `sm` | `xs` 12/18, `sm` 14/21 (default), `base` 16/24, `lg` 18/28. |
| `tone` | `"strong" \| "default" \| "destructive" \| "success" \| "muted"` | `default` | The 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.

| Token | Used for |
| --- | --- |
| `--border` | border, background |
| `--card` | background |
| `--destructive-strong` | text |
| `--foreground` | text |
| `--heading` | text |
| `--muted-foreground` | text |
| `--primary` | text, underline |
| `--ring` | outline |
| `--success-tag-foreground` | text |
