How Satori Turns JSX Into an OG Image (and Where It Falls Short)
A technical walkthrough of what Satori actually does — reimplementing CSS layout without a browser — and the specific CSS features it can't render, which shape what a satori og image can and can't look like.
August 16, 20267 min read
Satori is the JavaScript library that converts a JSX tree into an SVG image by computing CSS layout itself, without a browser — it's what actually powers @vercel/og and most "generate an OG image from JSX" setups in the Next.js ecosystem. Its approach is specific and somewhat unusual: it reimplements the CSS box model and flexbox layout algorithm in pure JavaScript rather than delegating to a browser engine. Understanding how a satori og image actually gets produced — and specifically where that approach runs out of road — makes it much easier to predict what a given template design will and won't be able to do before you spend time building it.
This kind of understanding matters more for OG images than for most rendering tasks, because the failure mode is invisible until someone shares a link. A layout bug in a normal web page gets caught the moment a developer looks at the page in a browser. A layout bug in a Satori-rendered OG image only shows up when someone actually shares the URL and looks at the resulting card in Slack, X, or iMessage — which means it's easy to ship a broken template and not notice for weeks.
How does Satori compute layout without a browser engine?
Every normal way you generate an image from HTML and CSS involves a real browser engine somewhere — Chromium via a headless-browser screenshot, WebKit, something that fully implements the CSS specification and DOM behavior the way a browser tab would. Satori doesn't do that. It takes a JSX tree and a set of CSS-like style properties and computes the resulting layout itself, in pure JavaScript, implementing its own version of the box model and (primarily) flexbox layout algorithm from scratch. There's no DOM, no page load, no JavaScript execution inside the rendered content, and no browser process spinning up anywhere in the pipeline.
That's the entire reason it's fast enough to run inside an Edge Runtime function on every request. Booting a headless browser, even a lightweight one, costs real time and memory — often hundreds of milliseconds to seconds, and a meaningfully larger runtime footprint. Reimplementing just the layout math needed for a card-shaped image is dramatically cheaper, which is what makes request-time image generation viable at all in a serverless or edge context.
From JSX to SVG to PNG
The pipeline has three stages. First, Satori walks your JSX tree, reads the style properties on each node, and computes where every element sits and how large it is, using its own flexbox implementation. Second, it emits that computed layout as SVG markup — text elements, rectangles, images, positioned exactly where the layout algorithm placed them. Third, something has to turn that SVG into a raster image, since most og:image consumers expect a PNG or JPEG rather than SVG directly; @vercel/og handles this with resvg, a Rust-based SVG renderer compiled for the runtime it's running in. The end-to-end result is JSX in, PNG out, with no browser anywhere in the chain — just two purpose-built rendering steps.
What CSS features does Satori support?
- Flexbox layout — this is Satori's core supported layout model, and it's implemented thoroughly enough to build most card-style compositions: rows, columns, alignment, wrapping, gaps.
- Basic box model properties — width, height, padding, margin, border, border-radius, background-color.
- Text — including line height, letter spacing, and text wrapping, though fonts must be explicitly supplied as font data rather than referenced by a system font name or a stylesheet link.
- Images — referenced by URL or as embedded data, positioned within the flex layout like any other element.
- Simple gradients and basic shadow properties, within limits — not every gradient or shadow configuration a browser would render is guaranteed to render identically.
What CSS features doesn't Satori support?
The most consequential gap is CSS Grid — Satori doesn't implement it, so any layout that genuinely needs a grid (rather than a flexbox arrangement that approximates one) has to be redesigned around flexbox or built as a fixed, manually positioned layout instead. CSS transforms (rotate, scale, skew) have limited support. Advanced selectors, pseudo-elements, animations, and anything that depends on real browser behavior — like how a specific font renders its fallback glyphs, or how a browser handles overflow in edge cases — aren't part of what Satori reimplements, because there's no browser present to fall back on. Custom web fonts also require you to load and pass font binary data yourself; there's no automatic font-loading step the way a browser page would have.
Why this tradeoff exists at all
It's worth being clear that this isn't a bug or an oversight — it's the deliberate tradeoff that makes Satori useful in the first place. A tool that fully implemented the CSS specification would need to essentially be a browser engine, which defeats the point of a lightweight, edge-runtime-compatible image generator. Satori's designers chose to implement the subset of CSS that covers the overwhelming majority of card-style, text-and-image compositions — which is genuinely most of what an OG image needs to be — and accept that anything requiring the long tail of CSS features simply isn't in scope.
Designing within the constraint
In practice, templates that work well with Satori tend to be built the way a print designer might think about a card: a background, a flexible arrangement of text and image blocks, clear alignment, minimal reliance on effects that depend on a real rendering engine. Trying to force a design built with CSS Grid or heavy use of transforms into Satori usually means fighting the tool rather than using it — the better path is either redesigning the template to fit flexbox's model, or accepting that the specific layout needs a different rendering approach entirely.
Debugging a Satori layout that isn't rendering as expected
Because Satori's flexbox implementation is its own reimplementation rather than a browser's, the most common source of confusion is assuming a CSS property behaves exactly the way it would in a real browser when it actually doesn't, or isn't supported at all. A layout that looks subtly wrong — text overflowing a box, elements not aligning the way they would in Chrome DevTools — is often not a bug in your JSX but a property Satori interprets differently or ignores outright. The reliable way to debug this is to strip a template down to its simplest form and add complexity back incrementally, checking the actual rendered PNG at each step, rather than assuming standard CSS intuition will predict Satori's output directly.
How does font loading work with Satori?
Because there's no browser to resolve a @font-face declaration or a system font stack, every font used in a Satori template has to be explicitly fetched as binary data (typically a .ttf or .otf file) and passed into the render call. This means a template using a custom brand font needs that font file available at render time — either bundled with the deployment or fetched from a URL before rendering — and needs a fallback if only certain weights or styles were loaded, since Satori won't synthesize a bold or italic variant that wasn't explicitly supplied the way a browser's font-matching might approximate.
Where useopengraph fits alongside this
Because useopengraph's templates aren't built by hand-authoring JSX against Satori's constraints, template design isn't limited to the same CSS subset — templates are built visually, with text, image, and color variables, and rendered server-side rather than inside your own Edge Runtime function. That removes the design ceiling that comes with reimplemented CSS layout, and it moves versioning, caching, and multi-platform rendering off your own infrastructure entirely — you get the same fast, request-time image generation Satori is good at, without inheriting its layout limitations or having to build the surrounding infrastructure (immutable URLs, CDN caching, a template editor) yourself.
Stop paying per seat
for a usage-shaped problem.
Unlimited teammates, one usage pool. Start free with the scanner — no card required.