Write documents in React. Print them with Typst.
HTML-to-PDF through a headless browser gives you web typography on paper: no hyphenation worth the name, widows and orphans, tables that split badly, and a Chromium download in your deploy. reactypst keeps the part of that workflow people like — components, props, loops, TypeScript — and replaces the printer. Your component tree becomes Typst markup; Typst typesets it the way a book is typeset.
import { Page } from "reactypst";
export default function Letter({ to }: { to: string }) {
return (
<Page size="a4" font="Libertinus Serif" numbering>
<h1>Dear {to},</h1>
<p style={{ textAlign: "justify" }}>Thank you for your order…</p>
</Page>
);
}npx reactypst letter.tsx -o letter.pdf # PDF through the typst CLI
npx reactypst letter.tsx --typ # just the .typ, to inspect or commit| React | Typst |
|---|---|
h1…h6 |
= … ====== headings |
p, div, section, header, footer, article |
paragraphs and block(...) |
strong/b, em/i, u, s, small, sup, sub |
*…*, _…_, underline, strike, text(size), super, sub |
ul, ol, li (nested) |
- and + lists |
table, tr, th, td |
table(columns: n, …) |
a, img, code, pre, blockquote, hr, br |
link, image, raw, quote, line, line break |
style={{ … }} |
text(size, fill, weight, font, tracking) and block(fill, inset, stroke, radius, width, height), align(...) |
Function components are called with their props, so composition, map, conditionals and shared layout components all work. Hooks that need a live renderer (state, effects) do not — a PDF has no second render.
Typst-only concepts come as components that preview harmlessly in a browser:
<Page size margin font fontSize numbering> |
#set page(...), #set text(...), justified paragraphs |
<PageBreak /> |
#pagebreak() |
<Columns count={2}> |
#columns(2)[…] |
<Raw>{"#lorem(40)"}</Raw> |
Typst passed through untouched |
Every element or CSS property without a clean Typst equivalent is reported once through onUnsupported (the CLI prints them). A box-shadow never just disappears from your invoice.
renderToTypst(<Invoice />, { onUnsupported: (what) => console.warn(what) });- Real line breaking and hyphenation, per language.
- Tables, footnotes, page numbers and running headers that behave like print.
- A single 30 MB binary instead of a headless browser; milliseconds per page.
service/ turns reactypst into a document server: save templates over GraphQL, queue renders, get signed S3 links to the PDFs. NestJS with Apollo, Prisma on PostgreSQL, RabbitMQ between the API and the render workers.
sequenceDiagram
participant C as client
participant A as API (Nest + Apollo)
participant Q as RabbitMQ
participant W as worker
C->>A: render(slug, props)
A->>Q: render id
A-->>C: { id, state: QUEUED }
Q->>W: render id
W->>W: TSX → Typst → PDF
W->>S3: put PDF
C->>A: renderJob(id)
A-->>C: { state: DONE, url }
npm run storybook shows every component with the Typst markup it produces, and a rendered page when the preview server is running.
npm install
npm test
npm run lint
npx tsx src/cli.ts examples/invoice.tsx --typ- Typst by Laurenz Mädje and Martin Haug does all the actual typesetting.
- The reconciler-free rendering approach is borrowed in spirit from
react-dom/serverand from Vercel'ssatori. - Libertinus fonts in the examples by Khaled Hosny and the Libertinus contributors.
MIT © Ivan Novikov

