# Typeset

Set rendered markdown, such as a chat answer or a document, in the kit’s type.

A chat answer with a code fence and a table

[Complete TSX file](/examples/typeset/default.tsx)

```tsx
"use client";

import { enableCodeHighlighting } from "@/siteplane/components/code/code-highlight";
import { Typeset, typesetComponents } from "@/siteplane/components/typeset";

// The fence names a language. Highlighting is optional; an app turns it on
// once, before the first Code Block renders.
enableCodeHighlighting();

// The parts a markdown renderer takes from the map, written out here by hand.
const {
  a: A,
  code: InlineCode,
  h3: H3,
  li: Li,
  p: P,
  pre: Pre,
  strong: Strong,
  table: Table,
  tbody: Tbody,
  td: Td,
  th: Th,
  thead: Thead,
  tr: Tr,
  ul: Ul,
} = typesetComponents;

const query = `SELECT name, active_seats, active_seats_12w_ago
FROM usage
WHERE active_seats <= active_seats_12w_ago * 0.6
ORDER BY active_seats * 1.0 / active_seats_12w_ago
LIMIT 3;
`;

const accounts = [
  ["Halden Logistics", "17", "41", "−59%"],
  ["Brightwave Studio", "8", "18", "−56%"],
  ["Ortega Foods", "14", "26", "−46%"],
] as const;

// A renderer passes the alignment of a table column as a style.
const end = { textAlign: "right" } as const;

export default function Example() {
  return (
    <Typeset className="w-full max-w-md">
      <H3>Three accounts are likely to churn</H3>
      <P>
        Active seats fell by <Strong>40% or more</Strong> over the last 12 weeks
        at three accounts on the <InlineCode>Growth</InlineCode> and{" "}
        <InlineCode>Scale</InlineCode> plans. The{" "}
        <A href="#usage-report">usage report for week 43</A> has the full list.
      </P>
      <Ul>
        <Li>
          <Strong>Halden Logistics</Strong> has open questions about its
          renewal.
        </Li>
        <Li>Brightwave Studio stopped inviting new people in September.</Li>
        <Li>Ortega Foods moved its team to a smaller plan.</Li>
      </Ul>
      <P>This is the query behind the list:</P>
      <Pre>
        <code className="language-sql">{query}</code>
      </Pre>
      <Table>
        <Thead>
          <Tr>
            <Th>Account</Th>
            <Th style={end}>Seats now</Th>
            <Th style={end}>12 weeks ago</Th>
            <Th style={end}>Change</Th>
          </Tr>
        </Thead>
        <Tbody>
          {accounts.map(([account, now, before, change]) => (
            <Tr key={account}>
              <Td>{account}</Td>
              <Td style={end}>{now}</Td>
              <Td style={end}>{before}</Td>
              <Td style={end}>{change}</Td>
            </Tr>
          ))}
        </Tbody>
      </Table>
      <P>I can draft a check-in email for each of them.</P>
    </Typeset>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/typeset
```

With npm: `npx shadcn@4.18.0 add @siteplane/typeset`. [Registry JSON](/registry/typeset.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/code-highlight
```

Code is installed with Typeset. `code-highlight` highlights the code fences; without it, a fence shows plain text. See [Code](/docs/components/code/#syntax-highlighting).

## Usage

```tsx
import {
  Typeset,
  TypesetHeading,
  TypesetParagraph,
} from "@/siteplane/components/typeset";

<Typeset>
  <TypesetHeading level={3}>Three accounts are likely to churn</TypesetHeading>
  <TypesetParagraph>
    Active seats fell by 40% or more in 12 weeks.
  </TypesetParagraph>
</Typeset>;
```

## Composition

```text
Typeset
├── TypesetHeading
├── TypesetParagraph
│   ├── TypesetStrong
│   ├── TypesetLink
│   └── Code
├── TypesetList
│   └── TypesetListItem
├── TypesetQuote
├── TypesetRule
├── TypesetCodeBlock
└── a table, from typesetComponents
```

`Typeset` sets the kit’s body type and stacks its blocks with a gap. With a markdown renderer you do not write the parts yourself: `typesetComponents` maps each element the renderer passes to its part. Inline code is the kit’s [Code](/docs/components/code/), a fence is a Code Block with the fence’s language, and a table is a [Native Table](/docs/components/native-table/), the plain HTML table in the look of [Table](/docs/components/table/), without hover, since nothing in written text acts. A long value in the text wraps wherever it has to, so it never makes the thread wider. A table scrolls sideways when it is wider than the text, and its cells wrap between words only.

## Markdown renderers

The kit has no markdown renderer. Pass `typesetComponents` to the `components` prop of your renderer and put its output in a `Typeset`. The examples on this page write out by hand the elements that a renderer passes to the map.

### react-markdown

react-markdown renders its elements without a container, so they are the blocks of the Typeset. Tables need the `remark-gfm` plugin.

```tsx
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Typeset, typesetComponents } from "@/siteplane/components/typeset";

export function Answer({ text }: { text: string }) {
  return (
    <Typeset>
      <Markdown components={typesetComponents} remarkPlugins={[remarkGfm]}>
        {text}
      </Markdown>
    </Typeset>
  );
}
```

### Streamdown

Streamdown renders markdown that is still arriving, and puts its blocks in a container of its own, which spaces them with a margin. Give that container `className="contents space-y-0"`: `contents` makes the blocks the Typeset’s own, so its flow spaces them, and `space-y-0` removes the margin.

A fence whose closing line has not arrived yet keeps its copy action disabled. Read that with Streamdown’s `useIsCodeFenceIncomplete` and pass it to `TypesetCodeBlock` as `streaming`.

```tsx
import type { ComponentProps } from "react";
import { Streamdown, useIsCodeFenceIncomplete } from "streamdown";
import {
  Typeset,
  TypesetCodeBlock,
  typesetComponents,
} from "@/siteplane/components/typeset";

// The renderer's node is not an attribute of the DOM element.
function Fence({
  node: _node,
  ...props
}: ComponentProps<"pre"> & { node?: unknown }) {
  return <TypesetCodeBlock {...props} streaming={useIsCodeFenceIncomplete()} />;
}

const components = { ...typesetComponents, pre: Fence };

export function StreamingAnswer({
  streaming,
  text,
}: {
  streaming: boolean;
  text: string;
}) {
  return (
    <Typeset>
      <Streamdown
        className="contents space-y-0"
        components={components}
        isAnimating={streaming}
      >
        {text}
      </Streamdown>
    </Typeset>
  );
}
```

## Densities

`density="chat"`, the default, is the kit’s body type for an answer in a thread: 14 px text on a 21 px line, with 14 px between blocks. `density="document"` is a size up for an article or an answer read on its own: 16 px text on a 27 px line, with 24 px between blocks. Headings step up from the body size in both.

A review in the document density

[Complete TSX file](/examples/typeset/document.tsx)

```tsx
"use client";

import { Typeset, typesetComponents } from "@/siteplane/components/typeset";

// The parts a markdown renderer takes from the map, written out here by hand.
const {
  blockquote: Quote,
  code: InlineCode,
  h1: H1,
  h2: H2,
  hr: Rule,
  li: Li,
  ol: Ol,
  p: P,
  ul: Ul,
} = typesetComponents;

export default function Example() {
  return (
    <Typeset className="w-full max-w-md" density="document">
      <H1>Churn review, week 43</H1>
      <P>
        Three accounts lost more than 40% of their active seats in 12 weeks.
        This review lists what we know and who reaches out first.
      </P>
      <H2>What changed</H2>
      <Ul>
        <Li>
          Seats fell fastest on the <InlineCode>Growth</InlineCode> plan:
          <Ul>
            <Li>Halden Logistics from 41 to 17 seats</Li>
            <Li>Brightwave Studio from 18 to 8 seats</Li>
          </Ul>
        </Li>
        <Li>Ortega Foods kept its plan but moved half of its team out.</Li>
      </Ul>
      <Quote>
        <P>
          We were told the reorganization would pause hiring, so we stopped
          adding seats.
        </P>
        <P>A support conversation with Halden Logistics, 12 October.</P>
      </Quote>
      <Rule />
      <H2>Who reaches out first</H2>
      <Ol>
        <Li>
          Halden Logistics, by the account owner, before the renewal call.
        </Li>
        <Li>Brightwave Studio, with the usage report attached.</Li>
        <Li>Ortega Foods, once the support thread is answered.</Li>
      </Ol>
    </Typeset>
  );
}
```

## Tuning

Three custom properties tune a Typeset, and a density sets all three: `--sp-typeset-size` is the size of the text, `--sp-typeset-leading` its line height and `--sp-typeset-flow` the space between blocks. Headings and the flow follow the size, so a new size alone keeps the proportions of the density. Set them with a utility class or a style on the Typeset; either wins over the density. Give the flow as a length, such as a token or a multiple of the size, not in `em`, which would grow with the text of a heading.

```tsx
<Typeset className="[--sp-typeset-leading:var(--sp-rw-line-24)] [--sp-typeset-size:var(--sp-rw-font-16)]">
  {answer}
</Typeset>
```

## Streaming

A reply that streams adds blocks at the end and grows its last block. Typeset spaces its blocks with a gap, and none of its rules looks at the blocks after an element, so a block that arrives never restyles the ones before it. In a fence that streams, the lines that are complete keep their highlighting while the last line grows.

## AI SDK

A `text` part is markdown. Render it in a Typeset with the map, and tell Streamdown that the part is still arriving while its `state` is `streaming`. As with the steps on the [Trace](/docs/components/trace/#ai-sdk) page, only the last message streams, and only while the chat’s `status` is `streaming`, so a part that stopped with the reply does not stay animated. In an assistant turn, the Typeset sits in a ghost [Bubble](/docs/components/bubble/), which gives the answer no surface of its own.

```tsx
import type { UIMessage } from "ai";

export function AnswerText({
  live,
  message,
}: {
  live: boolean;
  message: UIMessage;
}) {
  return message.parts.map((part, index) =>
    part.type === "text" ? (
      <StreamingAnswer
        key={index}
        streaming={live && part.state === "streaming"}
        text={part.text}
      />
    ) : null,
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Typeset.density | TypesetDensity: chat \| document | chat | chat is the body type with a tight rhythm, for a message; document is a size up with more room between blocks. |
| TypesetHeading.level | TypesetHeadingLevel: 1 \| 2 \| 3 \| 4 \| 5 \| 6 | 2 | The heading level, which is also the element. Levels 1 to 3 have a size of their own; 4 to 6 share the body size in a heavier weight. |
| TypesetList.ordered | boolean | false | A numbered list (ol) instead of a bulleted one (ul). |
| TypesetCodeBlock.streaming | boolean | false | The fence is still open: the copy action stays disabled until it closes. |
| typesetComponents | Record of element names to parts | — | The map for the components prop of react-markdown and Streamdown: a, blockquote, code, h1 to h6, hr, li, ol, p, pre, strong, the table elements and ul. |
`Typeset` renders a `div` that stacks its blocks with the flow as a gap. Each
part renders its element (`TypesetParagraph` a `p`, `TypesetListItem` an `li`,
`TypesetQuote` a `blockquote`, `TypesetStrong` a `strong`) and takes its props
and string `className`. `TypesetLink` is the kit’s inline link Button rendered
as an `a`, underlined in the text color, and takes the attributes of an anchor.
`TypesetRule` is the kit’s [Separator](/docs/components/separator/).
`TypesetCodeBlock` reads the `code` element a renderer puts in a `pre`: its
`language-x` class becomes the Code Block’s `language` and title, and its text
the value, which the Code Block copies. The table parts take no inline style
from the renderer: a column’s alignment becomes the cell’s alignment, and the
rest is dropped. The parts of the map drop the `node` prop a renderer passes.
`TypesetDensity`, `TypesetProps`, `TypesetHeadingLevel`, `TypesetHeadingProps`,
`TypesetListProps` and `TypesetCodeBlockProps` are exported.

[Read the installed source](/source/typeset).

## Usage notes

Typeset styles what the renderer gives it. Which markdown and HTML the renderer allows, and how it treats links and images, is the renderer’s. Elements the map leaves out, such as images, emphasis and strikethrough, render as the renderer renders them, in the type of the Typeset.

Typeset is for written text. The labels and text of an interface, such as a form or a card, take the type of their own components.
