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.
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-startonly).
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.
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.
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-startonly - 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).
| Variable | Type | Default |
|---|---|---|
| --typeset-font-body | font-family | inherit |
| --typeset-font-heading | font-family | var(--font-sans) |
| --typeset-font-mono | font-family | ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace |
| --typeset-size | length | 1em |
| --typeset-leading | number | 1.75 |
| --typeset-flow | length | 1.25em |
<div className="typeset typeset-docs"> <YourMarkdownRenderer>{content}</YourMarkdownRenderer></div>