# Prose

> Prose typography for rendered markdown in React and shadcn, with a chat density for AI replies and a document density for long reads.

Typography for rendered markdown and rich text, from one element map in two densities: chat for replies and document for long reads. Display headings, hairline tables and footnotes.

- Tier: Free (MIT)
- Interaction: Scroll
- Page: https://eased.dev/components/prose
- Registry: https://eased.dev/r/prose.json

## Install

```bash
npx shadcn add https://eased.dev/r/prose.json
```

Style rendered markdown and rich text in Eased's tokens by wrapping it once, with no component map and no global stylesheet.

## When to use

- Make an assistant reply or a comment read well: wrap your renderer's output (MDX, react-markdown, Streamdown, a CMS string) in `density="chat"`.
- Give docs, changelogs, policies and articles a longer-read rhythm with `density="document"`.
- Show inline code in UI copy outside Prose with the `Code` export.

## When not to use

- Single lines of UI copy: use [Text](/components/text).
- Page titles outside the rendered content: use [Heading](/components/heading).
- Code you want highlighted with a copy button: use [Code Block](/components/code-block), mapped onto `pre` in your renderer.

## Usage

```tsx release-notes.tsx
import { Prose } from "@/components/eased/prose/prose";

export function ReleaseNotes() {
  return (
    <Prose density="document" as="article">
      <h2>Webhooks v2</h2>
      <p>
        Deliveries now retry with <strong>exponential backoff</strong> for up to 72 hours. Read the{" "}
        <a href="/docs/signing">signing guide</a> before you upgrade.
      </p>
      <ul>
        <li>Payloads carry an <code>idempotency_key</code>, so a retried event is safe to process twice.</li>
        <li>Failed endpoints pause after 20 straight errors instead of 5.</li>
      </ul>
    </Prose>
  );
}
```

## Examples

### Chat reply

Wrap a markdown renderer's output for an assistant reply or a comment. The body is 14px and headings stay at UI sizes.

```tsx
<Prose density="chat">
  <Markdown>{reply}</Markdown>
</Prose>
```

### Inline code outside Prose

Use `Code` in UI copy, or `codeClass` on your own element, for the same inline-code look.

```tsx
import { Code } from "@/components/eased/prose/prose";

<p>Run <Code>pnpm build</Code> before you deploy.</p>
```

## API reference

### Prose

| Prop | Type | Default | Description |
|---|---|---|---|
| `density` | `"chat" \| "document"` | `"chat"` | `chat`: 14px body, UI-size headings and a tight rhythm. `document`: 16px body, `h1`–`h3` on the display scale (30, 24, 20px), a wider rhythm, and a lead paragraph after an `h1`. |
| `as` | `ElementType` | `"div"` | The element it renders. Use `"article"` for a standalone document. |
| `className` | `string` | `""` | Appended to the container's classes. |

Every other attribute of the `as` element passes through. The container also gets `data-density`.

It styles paragraphs, `strong`, `em`, `del`, `mark`, `small`, links, nested lists, blockquotes, rules, inline `code`, `pre` blocks, `kbd`, tables, images, figures and GFM footnotes. A heading sits close to what it introduces and far from what came before. Nothing outside the container is touched.

### Code

A `<code>` element with Prose's inline-code look: mono at 0.9em on a sunken tint. Takes every `<code>` attribute.

### codeClass

The class string `Code` uses, for applying the same look to another element.

## Accessibility

- Prose adds no roles: the semantics of your markup stay as they are, so headings keep their outline and tables keep their headers.
- Links show Eased's 2px focus outline on keyboard focus, and stay underlined so they don't rely on colour alone.
- Pass `as="article"` when the content stands on its own, such as a changelog entry.

## Responsive

Long words and URLs break instead of overflowing. Code blocks scroll sideways rather than wrapping, and table cells wrap so the page doesn't scroll horizontally; header cells stay on one line. Images never grow wider than the container.

## Motion

Links firm up their underline over 150ms on the standard curve on hover, on pointer devices only. Nothing else moves: Prose holds still while streaming text animates inside it.

## Notes for AI

Pick Prose to style any HTML you don't author element by element: markdown, MDX or a CMS string. Choose `chat` for replies and comments and `document` for long reads. To render code fences as Eased Code Blocks, map `pre` and `code` in your renderer, as Conversation's Response does.

Don't pass a component map just to add classes, and don't add `@tailwindcss/typography` or a global stylesheet: Prose already styles every element from the container. Don't nest one Prose inside another.

## Related

- [Text](/components/text): single pieces of UI copy.
- [Heading](/components/heading): titles outside rendered content.
- [Code Block](/components/code-block): highlighted code with a copy button.

## Example

`components/eased/prose/prose-demo.tsx`

```tsx
"use client";

import { useState } from "react";
import { DemoFrame } from "../lib/demo-frame";
import { Segmented } from "../segmented/segmented";
import { Prose, type ProseDensity } from "./prose";

/** One release note, read as a chat reply or as a document. */
export function ProseDemo() {
  const [density, setDensity] = useState<ProseDensity>("chat");
  return (
    <DemoFrame
      title="Release notes"
      actions={
        <Segmented<ProseDensity>
          aria-label="Density"
          value={density}
          onValueChange={setDensity}
          options={[
            { value: "chat", label: "Chat" },
            { value: "document", label: "Document" },
          ]}
        />
      }
    >
      <div className="min-h-0 flex-1 overflow-y-auto px-(--pad) py-4">
        <Prose density={density} as="article">
          <h2>Webhooks v2</h2>
          <p>
            Deliveries now retry with <strong>exponential backoff</strong> for up to 72 hours, and every attempt is
            signed. Read the <a href="#signing">signing guide</a> before you upgrade.
          </p>
          <h3>What changed</h3>
          <ul>
            <li>
              Payloads carry an <code>idempotency_key</code>, so a retried event is safe to process twice.
            </li>
            <li>
              Failed endpoints pause after 20 straight errors instead of 5.
              <ul>
                <li>Paused endpoints resume from the dashboard or the API.</li>
              </ul>
            </li>
            <li>The legacy <code>X-Signature</code> header is deprecated.</li>
          </ul>
          <pre>
            <code>{`curl -X POST https://api.acme.dev/v2/webhooks \\
  -H "Authorization: Bearer $ACME_KEY" \\
  -d url=https://example.com/hooks`}</code>
          </pre>
          <h3>Retry schedule</h3>
          <table>
            <thead>
              <tr>
                <th>Attempt</th>
                <th>Delay</th>
                <th>Cumulative</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>1</td>
                <td>30 s</td>
                <td>30 s</td>
              </tr>
              <tr>
                <td>2</td>
                <td>5 min</td>
                <td>5 min 30 s</td>
              </tr>
              <tr>
                <td>3</td>
                <td>1 h</td>
                <td>1 h 5 min</td>
              </tr>
            </tbody>
          </table>
          <blockquote>
            <p>Retries saved us from re-running a whole day of billing events after a 40-minute outage.</p>
          </blockquote>
          <hr />
          <p>
            Questions? Reply here or press <kbd>?</kbd> for shortcuts.
          </p>
        </Prose>
      </div>
    </DemoFrame>
  );
}
```
