BlockNote DocsFeaturesExportPDF

PDF Export

It's possible to export BlockNote documents to PDF, completely client-side. The exporter is powered by the Typst typesetting engine (compiled to WebAssembly) and produces accessible, tagged PDF/UA-1 documents: the PDF carries a logical structure tree (headings, paragraphs, lists, tables, figures with alt text, links) that screen readers can navigate.

This feature is provided by the @blocknote/xl-pdf-exporter. xl- packages are fully open source, but released under a copyleft license. A commercial license for usage in closed source, proprietary products comes as part of the Business subscription.

First, install the @blocknote/xl-pdf-exporter package:

npm install @blocknote/xl-pdf-exporter

Then, create an instance of the PDFExporter class and export the document:

import {
  PDFExporter,
  typstDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter";

// Create the exporter
const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings);

// Export the document to a PDF Blob (there's also `toBytes` for a Uint8Array)
const blob = await exporter.toBlob(
  editor.document,
  {},
  { title: "My document", lang: "en" },
);

By default the Typst compiler (a ~30MB wasm file) and its stock fonts are loaded from a CDN on the first export — handy to get started, but for production you'll want to bundle them, which also makes the export match the editor's own font: see Fonts & offline use.

When repeatedly exporting changing content (e.g. a live preview), create a fresh exporter per export — construction is cheap, and an exporter instance accumulates the image assets it has resolved for as long as it lives.

See the full example with a live PDF preview below:

Customizing the PDF

The second parameter of toBlob / toBytes takes the compile options (covered in Fonts & offline use); the third takes per-document options:

const blob = await exporter.toBlob(editor.document, {}, {
  // Document title - required for PDF/UA (also shown in the viewer's title bar)
  title: "My document",
  // Document author, written to the PDF metadata
  author: "John Doe",
  // BCP-47 language tag of the document's natural language
  lang: "en",
  // Typst paper name, e.g. "a4" (default) or "us-letter"
  paper: "a4",
  // Page margin as a Typst length
  margin: "48pt",
  // Raw Typst markup for the running page header / footer, e.g. a
  // page counter: "#context counter(page).display()"
  header: "My document",
  footer: "#context counter(page).display()",
});

Custom mappings / custom schemas

The PDFExporter constructor takes a schema and mappings parameter. A mapping defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) — for this exporter, into a Typst markup string. The same mappings drive the standalone Typst export, so one custom-block mapping serves both formats.

If you're using a custom schema in your editor, or if you want to overwrite how default BlockNote elements are converted, you can pass your own mappings:

import {
  PDFExporter,
  typstDefaultSchemaMappings,
  strLit,
} from "@blocknote/xl-pdf-exporter";

new PDFExporter(schema, {
  ...typstDefaultSchemaMappings,
  blockMapping: {
    ...typstDefaultSchemaMappings.blockMapping,
    myCustomBlock: (block, exporter) => {
      // Return Typst markup; `strLit` safely embeds user text as a
      // Typst string literal.
      return `#${strLit("My custom block")}`;
    },
  },
});

For a block with inline content, render it the way the default mappings do: exporter.transformInlineContent(block.content).join("") (inline results are markup strings, so plain concatenation composes them).

Math & diagram blocks

The math and diagram blocks ship Typst mappings — math exports as native Typst equations (real text, not images), diagrams as embedded vector SVG — both carrying alt text, as PDF/UA requires:

import { diagramBlockMapping } from "@blocknote/diagram-block/typst-exporter";
import {
  inlineMathMapping,
  mathBlockMapping,
} from "@blocknote/math-block/typst-exporter";

new PDFExporter(editor.schema, {
  ...typstDefaultSchemaMappings,
  blockMapping: {
    ...typstDefaultSchemaMappings.blockMapping,
    mathBlock: mathBlockMapping,
    diagram: diagramBlockMapping,
  },
  inlineContentMapping: {
    ...typstDefaultSchemaMappings.inlineContentMapping,
    math: inlineMathMapping,
  },
});

Fonts & offline use

The compile options (the second parameter of toBlob / toBytes) control what gets loaded into the Typst compiler:

import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url";

const blob = await exporter.toBlob(editor.document, {
  // The compiler wasm, bundled by your bundler (Vite shown here) instead
  // of loaded from a CDN. Install @myriaddreamin/typst-ts-web-compiler to
  // import it.
  getModule: () => compilerWasmUrl,
  // Font bytes to load into the compiler - e.g. BlockNote's Inter and
  // Geist Mono, and a math font (New Computer Modern Math) when the
  // document contains math blocks.
  fonts: [interRegular, interBold, geistMono, newCMMath],
  // An emoji-capable font. Browsers give the compiler no access to OS
  // fonts, so without one emoji render as missing glyphs (and fail PDF/UA).
  emojiFont: notoColorEmoji,
  // Skip fetching Typst's stock fonts from its CDN - with the fonts above
  // bundled, the export runs fully offline.
  preloadDefaultFonts: false,
});

The wasm and fonts are loaded once, on the page's first export, and reused afterwards — pass every font the page will need on that first call. The example bundles all of the above and works fully offline.

To match the editor's look, set the exporter's font families to the fonts you loaded (defaults: "Inter 18pt" body, "Geist Mono" code). This is also how you cover scripts the primary font doesn't, e.g. CJK — load the extra font's bytes and declare a fallback list:

const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, {
  fontFamily: ["Inter 18pt", "Noto Sans SC"],
  // The family name of the font passed via the `emojiFont` compile option -
  // listing it makes multi-codepoint emoji (e.g. 🚶‍♀️) shape correctly.
  emojiFontFamily: "Noto Color Emoji",
});

PDF/UA conformance

The produced PDF is tagged and declares PDF/UA-1 conformance. Two things to know:

  • Alt text: every image needs it. BlockNote's image block has no dedicated alt field yet, so the caption (or file name) is used — give images captions.
  • Headings: PDF/UA requires the document's first heading to be level 1 — start documents with an H1.

The declaration doesn't itself guarantee conformance of arbitrary input, so validate exports with veraPDF (--flavour ua1) if conformance matters to you. For a document known not to conform, pass declarePdfUA: false in the compile options to produce an honest tagged-but-unclaimed PDF instead of a false claim.

Exporter options

The PDFExporter constructor takes an optional third options parameter:

const defaultOptions = {
  // a function to resolve external resources (e.g. images) in order to avoid
  // CORS issues; by default, this calls a BlockNote hosted server-side proxy
  resolveFileUrl: corsProxyResolveFileUrl,
  // the strings rendered into the exported document (file link texts, error
  // placeholders); pass a locale from @blocknote/core/locales (or your
  // editor's dictionary) to export in another language
  dictionary: locales.en,
  // the colors used for highlighting, background colors and font colors
  colors: COLORS_DEFAULT, // defaults from @blocknote/core
  // font families, see "Fonts & offline use" above
  fontFamily: "Inter 18pt",
  monoFontFamily: "Geist Mono",
  // base font size in points
  fontSize: 12,
};

Exporting Typst markup

The underlying Typst source export is available standalone (e.g. to compile with your own Typst toolchain, including server-side) — see Typst export.

Deprecated: the react-pdf exporter

Previous versions of @blocknote/xl-pdf-exporter exported PDFs with react-pdf, producing untagged (not accessible) documents. That exporter is deprecated and will be removed after a few releases; until then it remains available unchanged from the @blocknote/xl-pdf-exporter/react-pdf subpath:

import {
  PDFExporter,
  pdfDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter/react-pdf";

Note that its mappings are react-pdf mappings — when migrating to the new exporter, custom blocks need a Typst mapping instead.