Typography

Flexible prose styles for rendered HTML and markdown.

Style headings, paragraphs, lists, and more inside a typeset container. When prose includes interface blocks such as Card, opt them out of Typeset as needed.

Typeset

A styling system for HTML and rendered markdown. One CSS file you own — wrap content in typeset and the rhythm follows.

Rhythm

Three controls drive everything: size, leading, and flow. Headings, lists, and rules derive from them.

  • Fits its container
  • Uses your theme tokens
  • Stable for streaming appends

Spacing flows with margin-block-start only — new blocks do not restyle earlier ones.

tsx
export function Example() {  return (    <div className="typeset typeset-docs">      <h1>Typeset</h1>      <p>        A styling system for HTML and rendered markdown. One CSS file you own —        wrap content in <code>typeset</code> and the rhythm follows.      </p>      <h2>Rhythm</h2>      <p>        Three controls drive everything: <strong>size</strong>,{" "}        <strong>leading</strong>, and <strong>flow</strong>. Headings, lists,        and rules derive from them.      </p>      <ul>        <li>Fits its container</li>        <li>Uses your theme tokens</li>        <li>Stable for streaming appends</li>      </ul>      <blockquote>        <p>Spacing flows with margin-block-start only — new blocks do not restyle earlier ones.</p>      </blockquote>    </div>  )}

Installation

Install the package, then import Typeset after Tailwind.

npm i @arctis-sh/ui@latest
@import "tailwindcss";@import "@arctis-sh/ui/styles/typeset.css";

Wrap your rendered HTML or markdown.

<div className="typeset typeset-docs">  <YourMarkdownRenderer>{content}</YourMarkdownRenderer></div>

typeset enables the styles. typeset-docs and typeset-chat are presets included with the stylesheet.

Principles

Three controls set the rhythm: size, leading, and flow. Heading sizes, list indents, and spacing around rules all derive from them.

Features

  • Fits its container. Chat bubbles inherit smaller type while articles scale up. Below 48rem, the size gets a small readability bump.
  • Uses your theme. Colors and radius come from --color-* and --radius. Dark mode follows your tokens.
  • Easy to tune. Change a preset and the whole document follows.
  • Streaming-safe. New blocks do not restyle earlier ones (no :last-child / :has() in layout rules; margin-block-start only).

Custom Typesets

Defaults live on .typeset. Override with a preset class:

.typeset-chat {  --typeset-flow: 1em;  --typeset-leading: 1.6;}.typeset-docs {  --typeset-size: 15px;  --typeset-flow: 1.5em;}

typeset-docs

Docs rhythm

Roomier flow for articles and documentation.

typeset-chat

Chat rhythm

Tighter leading and flow for message bubbles.

tsx
export function Example() {  return (    <div className="flex flex-col gap-8">      <div>        <p className="mb-2 text-xs tracking-wide text-muted-foreground">          typeset-docs        </p>        <div className="typeset typeset-docs">          <h3>Docs rhythm</h3>          <p>Roomier flow for articles and documentation.</p>        </div>      </div>      <div>        <p className="mb-2 text-xs tracking-wide text-muted-foreground">          typeset-chat        </p>        <div className="typeset typeset-chat">          <h3>Chat rhythm</h3>          <p>Tighter leading and flow for message bubbles.</p>        </div>      </div>    </div>  )}

For a one-off adjustment, override the variable directly.

<article className="typeset [--typeset-flow:1.75em]">...</article>

Responsive Table

Tables wrap by default. For horizontal scrolling, place one inside typeset-scroll.

<div className="typeset-scroll">  <table>...</table></div>

Overrides

Typeset lives in the components layer and uses :where() selectors, so Tailwind utilities take precedence without !important.

<div className="typeset typeset-docs">  <p className="text-lg">...</p></div>

Opting Out

Add not-typeset or data-not-typeset to keep a subtree unstyled:

Styled prose sits in the typeset container.

Untouched card
Opt out with not-typeset or data-not-typeset.
tsx
import {  Card,  CardDescription,  CardHeader,  CardTitle,} from "@/components/ui/card"export function Example() {  return (    <div className="typeset typeset-docs">      <p>Styled prose sits in the typeset container.</p>      <Card className="not-typeset mt-4">        <CardHeader>          <CardTitle>Untouched card</CardTitle>          <CardDescription>            Opt out with not-typeset or data-not-typeset.          </CardDescription>        </CardHeader>      </Card>    </div>  )}
import {  Card,  CardDescription,  CardHeader,  CardTitle,} from "@arctis-sh/ui/card"<div className="typeset">  <p>Styled prose.</p>  <Card className="not-typeset">    <CardHeader>      <CardTitle>Untouched card</CardTitle>      <CardDescription>        Opt out with not-typeset or data-not-typeset.      </CardDescription>    </CardHeader>  </Card></div>

Streaming

  • No forward-looking selectors in layout rules
  • Spacing uses margin-block-start only
  • Table separators sit on cells so appending a row does not restyle the row above

API Reference

Rhythm and font variables on .typeset (and presets that override them).

VariableTypeDefault
--typeset-font-bodyfont-familyinherit
--typeset-font-headingfont-familyvar(--font-sans)
--typeset-font-monofont-familyui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace
--typeset-sizelength1em
--typeset-leadingnumber1.75
--typeset-flowlength1.25em
<div className="typeset typeset-docs">  <YourMarkdownRenderer>{content}</YourMarkdownRenderer></div>