Svelte 5 components and Markdown helpers for rendering Typst as SVG.
- Svelte
>= 5.36 - Svelte async rendering enabled:
compilerOptions.experimental.async = true - Vite/SvelteKit-compatible asset handling for browser WASM imports
// svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true
}
}
};pnpm add typlete<script lang="ts">
import { TypstInline, TypstBlock } from 'typlete';
</script>
<p>
Inline formula: <TypstInline source="integral_0^1 x^2 dif x" />
</p>
<TypstBlock source="sum_(i=1)^n i = (n(n+1)) / 2" />source is Typst math by default. Typlete wraps math input in Typst math delimiters and escapes delimiter-breaking characters ($ and #) before compiling. Do not include outer $...$; pass the formula body.
Use inputMode="raw" for full Typst markup fragments that should not be wrapped as math. Raw mode passes the source to Typst unchanged, so user-controlled raw input needs separate policy around allowed Typst features and assets.
<TypstBlock inputMode="raw" source={'#rect(radius: 6pt, inset: 10pt)[#strong[Raw Typst]]'} /><!-- math mode: Typlete wraps source in Typst math -->
<TypstInline source="alpha + beta" />
<!-- raw mode: Typlete passes source to Typst unchanged -->
<TypstInline inputMode="raw" source={'$alpha + beta$'} />inputMode="markup" is accepted as a compatibility alias for raw.
Typst, TypstInline, and TypstBlock support these common props:
type TypstMode = 'inline' | 'block';
type TypstInputMode = 'math' | 'raw' | 'markup';
type TypstSvgSanitizer = (svg: string) => string;
source?: string;
inputMode?: TypstInputMode;
preamble?: string;
textSize?: string;
pageMargin?: string;
cache?: boolean;
sanitize?: TypstSvgSanitizer;
ariaLabel?: string;
title?: string;
loadingLabel?: string;
throwOnError?: boolean;
errorMode?: 'badge' | 'none';
class?: string;Typst also accepts mode. TypstInline and TypstBlock set mode automatically.
During SSR/build, Typlete renders SVG on the server. After hydration, the browser keeps the server-rendered SVG and loads the Typst WASM runtime only when the formula has to be rendered again on the client.
The browser compiler and renderer WASM files are imported as Vite assets. Consumers do not need to copy WASM files manually.
By default, render failures keep the last successful SVG visible and show a small error badge.
<TypstBlock source={formula} errorMode="badge" />
<TypstBlock source={formula} errorMode="none" />Use throwOnError={true} when server-side render failures should fail the request/build instead of being converted to component state.
import { renderTypstSvgServer } from 'typlete/server';
const svg = await renderTypstSvgServer({
source: 'integral_0^1 x^2 dif x',
mode: 'block',
inputMode: 'math',
preamble: '',
textSize: '11pt',
pageMargin: '0pt',
cache: true
});Typlete only renders namespaced Typlete fences. Normal typst fences stay normal Markdown code blocks, so documentation can show Typst source without triggering rendering.
Raw Typst render block:
```typlete-typst
#set text(size: 12pt)
#rect[hello]
```Math render block. The content is treated as a formula body; delimiter-breaking $ and # characters are escaped before Typst compiles it:
```typlete-math
sum_(i=1)^n i = (n(n+1)) / 2
```Accepted render fence names:
typlete -> raw Typst render block
typlete raw -> raw Typst render block
typlete typst -> raw Typst render block
typlete-typst -> raw Typst render block
typlete-raw -> raw Typst render block
typlete math -> math render block
typlete-math -> math render block
typlete-typst-math -> math render blockTypst source code remains source code:
```typst
#set text(size: 12pt)
#rect[hello]
```For inline formulas in MDsveX, use the Svelte component directly. The same math escaping applies:
The value is <TypstInline source="alpha + beta" />.import { transformTypleteMarkdown } from 'typlete/markdown';
const output = await transformTypleteMarkdown(markdown, {
output: 'component'
});Output modes:
type TypleteMarkdownOutput = 'component' | 'html' | 'markdown-image' | 'asset';Best for MDsveX/Svelte-aware Markdown pipelines.
await transformTypleteMarkdown(markdown, {
output: 'component'
});Produces Svelte component tags:
<TypstBlock source={'#rect[hello]'} inputMode="raw" />
<TypstBlock source={'alpha + beta'} />Best for renderers that accept raw HTML/SVG.
await transformTypleteMarkdown(markdown, {
output: 'html'
});This renders SVG immediately and inserts it into the Markdown output.
Best for renderers that do not support raw HTML but accept Markdown images.
await transformTypleteMarkdown(markdown, {
output: 'markdown-image'
});This renders SVG and inserts it as a data:image/svg+xml Markdown image.
Best for static sites.
await transformTypleteMarkdown(markdown, {
output: 'asset',
assetDir: 'static/typlete',
assetBaseUrl: '/typlete'
});This writes SVG files to assetDir and inserts normal Markdown image links.
Run the Typlete preprocessor before MDsveX so namespaced Typlete render fences are converted to Svelte component tags before MDsveX compiles the document.
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { mdsvex } from 'mdsvex';
import { createTypstMdsvexPreprocessor } from 'typlete/markdown';
const config = {
extensions: ['.svelte', '.svx', '.md'],
preprocess: [
createTypstMdsvexPreprocessor({
output: 'component'
}),
mdsvex({
extensions: ['.svx', '.md']
})
],
compilerOptions: {
experimental: {
async: true
}
},
kit: {
adapter: adapter()
}
};
export default config;For output: 'component', the preprocessor injects this import when a document contains a rendered Typst fence:
<script lang="ts">
import { TypstInline, TypstBlock } from 'typlete';
</script>Disable injection only if another MDsveX hook already imports the components:
createTypstMdsvexPreprocessor({
output: 'component',
injectComponentImports: false
});Markdown helpers accept the same render options as component/server rendering:
await transformTypleteMarkdown(markdown, {
output: 'asset',
assetDir: 'static/typlete',
assetBaseUrl: '/typlete',
preamble: '',
textSize: '11pt',
pageMargin: '0pt',
cache: true
});Typlete inserts compiler-produced SVG inline. By default it strips embedded SVG <script> tags and common script-like SVG attributes from that output. This is output-level defense-in-depth, not a sandbox for arbitrary raw Typst input or external assets. For stricter SVG sanitizing, pass a sanitizer.
DOMPurify is optional:
import { createDomPurifySvgSanitizer } from 'typlete/sanitizers/dompurify';
const sanitize = await createDomPurifySvgSanitizer();Server-side DOMPurify requires both dompurify and jsdom:
import { createServerDomPurifySvgSanitizer } from 'typlete/server/dompurify';
const sanitize = await createServerDomPurifySvgSanitizer();- Component SSR depends on Svelte experimental async rendering.
- Plain
typstfences are never render instructions; usetyplete-typst,typlete-raw, ortyplete-mathwhen Markdown should render Typst. - Math mode and
typlete-mathescape$and#before wrapping the source in Typst math delimiters. Raw mode andpreambleare passed to Typst unchanged. - Server rendering temporarily guards Typst runtime fetches so SvelteKit does not track external runtime fetches during SSR.
html,markdown-image, andassetoutput modes pre-render SVG immediately and are not reactive on the client.