Library / SDK
vercel/satori avatar
vercel/satori

Satori renders JSX to an SVG string with its own layout engine, and skips the browser entirely

Enlightened library to convert HTML and CSS to SVG

14,186 stars405 forksTypeScriptMPL-2.0

At a glance

What is it?
Satori takes a JSX element plus width, height and fonts, and returns an SVG string laid out by its own engine based on the SVG 1.1 spec. It is built for fixed cards rather than pages: React state and effects do not run, the element list stops at static and visible tags, and the output is not promised to match what a browser draws.
Who is it for?
Satori fits teams generating Open Graph cards, social images and server rendered SVG where the layout is Flexbox and the content is fixed. It is the wrong tool for anything that depends on state, effects, or the wider CSS surface.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • vercel/satori14,186 ★ · +219
Star history
Repository2026-09-222026-11-13
vercel/satori14k14.2k

219 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

One call returns an SVG string, with no browser in the path

Calling satori() takes a JSX element and an options object, and gives back an SVG string rather than a file on disk. The README renders hello, world at 600 by 400 with a color, and the result looks like '<svg ...><path d="..." fill="black"></path></svg>', with the text turned into a path. No browser, no DOM, and no screenshot step sits in that path. Layout calculation, font handling, and typography all happen inside the library, which is the reason it can run on a server. The call has two required shapes: the element itself, and an options object carrying width, height, and a fonts array. The package name comes from package.json (satori, published as MPL-2.0) and the quickest first look is the playground at og-playground.vercel.app; for turning the SVG into PNG Open Graph cards the README redirects to Vercel's Open Graph Image Generation docs.

jsx
// api.jsx
import satori from 'satori'

const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    width: 600,
    height: 400,
    fonts: [
      {
        name: 'Roboto',
        // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.
        data: robotoArrayBuffer,
        weight: 400,
        style: 'normal',
      },
    ],
  },
)

Yoga does the layout, and the wasm file rides along in the package

Satori implements its own layout engine based on the SVG 1.1 spec and supports a limited subset of HTML and CSS. A WebAssembly file ships with the package: the files field publishes the whole dist directory plus layout.wasm, and the export map exposes ./layout.wasm next to the main, ./standalone, ./experimental, ./jsx, and ./jsx/jsx-runtime entry points. What that costs a reader is specificity: properties outside the subset have no fallback here, so a template leaning on them comes out rearranged rather than refused.

JSX has to be pure and stateless, or it will not run

The element you pass must be pure and stateless. Custom React components are fine, but the APIs that make a component dynamic are not supported, and the README names useState and useEffect as unsupported, and allows dangerouslySetInnerHTML only on <style> and <script>. There is no state transition between render and output, no fetching inside the tree, and no way to hand it a server rendered HTML string and have that analyzed. Every value a card shows has to exist before the call. For teams that do not want React in the dependency tree, an experimental builtin JSX runtime can be enabled per file with @jsxImportSource pragmas instead of a global setting.

No <input>, no cursor, no <style>, nothing fetched at render time

HTML support is deliberately narrow, and the reason offered is Satori's own use case rather than a browser's. Only static and visible elements and properties are implemented. The README puts the cursor property out of consideration; text inputs show their value or placeholder and <style> elements apply their CSS, but external resources pulled through <link> or <script> are not loaded. The complete set of supported elements and their preset styles sits in a source file, src/handler/presets.ts, rather than in prose. The consequence for an author is a render closed to the network: nothing is fetched at render time, so whatever a card needs must be inlined or passed in, and gaps have to be found by comparing your templates against that file instead of by reading an error.

Fonts and images come in as data, never as a link to resolve

Fonts arrive as buffers, not as family names the renderer resolves on its own. Each entry in the fonts array carries a name, data, a weight, and a style, and the comment in the README says where the buffer comes from: read the file with fs in Node.js, or use fetch, then provide a Buffer or ArrayBuffer. A family you forget to pass has no source inside the render, so text naming it is yours to catch. Images follow the same reasoning, and for that reason the README pushes encoded data: base64 encoded image data, a buffer, or an arrayBuffer supplied as props.src avoids extra I/O inside Satori when you are heading for another format such as PNG. A background-image is stretched to fit its element unless you set the size.

The SVG is not promised to match what a browser renders

Satori does not promise that its SVG will match browser rendered HTML output, and it gives the reason without hedging: it implements its own layout engine based on the SVG 1.1 spec. That limit shapes how the tool should be used. Text arrives as paths, so glyph metrics come from the font you supplied instead of a system font stack, and anywhere the browser and Satori disagree the difference is yours to inspect. For social cards that means a visual check per template.

Canvas WebGL sits behind satori/experimental and runs shaders on the CPU

The canvas element has a WebGL path, and it is the most experimental corner of the library. It is reachable only from satori/experimental, chosen so it does not add to the size of the satori import, and the README warns it may change or be removed in any release. Each canvas gets a new WebGL2 context that is destroyed along with everything drawn into it, unless you pass a createWebGLContext(width, height) option and supply contexts yourself, which Satori then leaves alone. Contexts come from OffscreenCanvas in browsers and Web Workers. In Node.js 20.16+ it loads the gl package from your project, which must be a WebGL2 build of headless-gl such as an ANGLE and SwiftShader one, shaders run on the CPU there, and bundling server code means keeping gl external and shipping its files in the deployment.

Three releases on one day, and a version placeholder in package.json

Release activity is high and the version numbers move fast. Three releases landed within two days: 0.63.0 on October 19, 2026, then 0.63.1 and 0.63.2 on October 20, and the last push to the default branch main followed on October 23. For anyone whose images render in production, that cadence is an argument for pinning an exact version instead of floating on a range. The published package is MPL-2.0, and the version field in package.json reads 0.0.0-development.2, a placeholder rather than a shipped release, with release.config.cjs in the root and a pnpm workspace, turbo.json, and two vitest configs alongside it. A playground directory in the repository matches the homepage og-playground.vercel.app, which is the fastest way to see a template render before it goes near a server route.

Editorial conclusion

Satori fits teams generating Open Graph cards, social images and server rendered SVG where the layout is Flexbox and the content is fixed. It is the wrong tool for anything that depends on state, effects, or the wider CSS surface. Before adopting, check the supported element list in src/handler/presets.ts against your own templates, confirm your deployment can ship the gl package if you need canvas WebGL, and pin an exact version, since 0.63.0, 0.63.1 and 0.63.2 landed on October 19 and 20, 2026.

Frequently asked questions

How to use Satori?

Pass a JSX element and an options object to satori(), which returns an SVG string. The options object in the basic example carries width, height, and a fonts array, with each font entry holding a name, data, a weight, and a style.

Do I need React installed to use Satori?

There is an experimental builtin JSX runtime that works without installing React, enabled per file with @jsxImportSource pragmas pointing at satori/jsx. Custom React components are accepted as well, as long as they stay pure and stateless.

Why does my Satori SVG look different from the browser?

Satori implements its own layout engine based on the SVG 1.1 spec and does not guarantee the SVG will 100% match browser rendered HTML output.

Can Satori load a stylesheet or webfont at render time?

No. External resources through link or script are not loaded, though <style> elements apply their CSS, and only a limited subset of static and visible elements and properties is implemented, with the cursor property out of consideration. The supported element list is kept in src/handler/presets.ts.

How do I supply a font to Satori?

Add it to the fonts option. Each entry needs a name, data read as a Buffer or ArrayBuffer using fs in Node.js or fetch, a weight, and a style. Nothing in the render resolves a font family for you.

Is the WebGL canvas feature part of the main Satori import?

It is only available from satori/experimental, so it does not add to the size of the satori import. The plain satori import ignores the webgl prop and draws canvas like any other element, and the feature is experimental and may change or be removed in any release.

Official sources

  1. License: MPL-2.0
  2. Project website
  3. README
  4. Releases
  5. vercel/satori on GitHub
Community

Where developers are discussing it

Posts on Hacker News, dev.to and Lobsters that link to this repository, found by our scan of those communities. 1 in the last 30 days; newest first.

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vercel-satori.svg)](https://hysenlabs.com/projects/vercel-satori)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/vercel-satori.svg)](https://hysenlabs.com/projects/vercel-satori)

Was this useful?