Outputs
Image
Render a Canvas root to a PNG or SVG with @hyper-elements/react/image: social cards, Open Graph images and banners, on the server with no browser.
A <Canvas> root is an image of a fixed size. renderImage() from @hyper-elements/react/image writes it as a PNG or an SVG, on your server, with Satori and resvg. No browser, no screenshot service, no network.
import { readFile, writeFile } from "node:fs/promises";
import { Canvas, Circle, Heading, Image, Text, Row, Svg } from "@hyper-elements/react";
import { renderImage } from "@hyper-elements/react/image";
function PostCard({ title, author }: { title: string; author: string }) {
return (
<Canvas width={1200} height={630} style={{ backgroundColor: "#0f172a", padding: 64, gap: 24, color: "#ffffff", fontFamily: "Inter, sans-serif", justifyContent: "space-between" }}>
<Image src={{ src: "https://example.com/logo.png", width: 240, height: 80 }} alt="Acme" style={{ width: 120 }} />
<Heading level={1} style={{ fontSize: 64 }}>{title}</Heading>
<Row style={{ gap: 16, alignItems: "center" }}>
<Svg width={40} height={40} style={{ flex: "none" }}>
<Circle cx={20} cy={20} r={20} style={{ fill: "#38bdf8" }} />
</Svg>
<Text style={{ fontSize: 28, color: "#cbd5e1" }}>{author}</Text>
</Row>
</Canvas>
);
}
const { image, width, height, diagnostics } = await renderImage(<PostCard title="Launch week" author="Ada Lovelace" />, {
fonts: [
{ family: "Inter", src: await readFile("fonts/Inter-Regular.ttf") },
{ family: "Inter", src: await readFile("fonts/Inter-Bold.ttf"), weight: 700 },
],
images: { "https://example.com/logo.png": await readFile("images/logo.png") },
});
await writeFile("card.png", image);
Use it for social cards, Open Graph images, banners, or any image you’d otherwise build in a design tool and export by hand. Pass a different title and you have a card per blog post.
Install
npm install @hyper-elements/react satori @resvg/resvg-js react react-dom
| Peer dependency | Version | Why |
|---|---|---|
satori |
^0.33.5 (optional: only for /image) |
Lays out the Canvas and draws its text as paths, as SVG |
@resvg/resvg-js |
^2.6.2 (optional: only for /image) |
Draws that SVG as a PNG |
react, react-dom |
>=18 |
/image is an entry of @hyper-elements/react. Its engines are optional peer dependencies, so a project that doesn’t import /image installs nothing for them, and the main entry never loads them.
It runs in Node.js only. If it loads a second copy of @hyper-elements/react that lays templates out in another version, renderImage() throws an ElementsError (image/version) rather than drawing the template wrong.
Note: Satori runs WebAssembly, which a Content-Security-Policy without
'wasm-unsafe-eval'blocks. That’s one reason image output is its own entry,@hyper-elements/react/image, which the main entry never loads.
The Canvas
| Prop | What it does | Default |
|---|---|---|
width |
The image’s width in px | 1200 |
height |
The image’s height in px. What’s past it is cut | 630 |
title, lang, dir, fonts |
As on every root |
Its content is laid out as on a page, with no phone layout. Beside the text styles, backgroundColor, gap and alignItems, a Canvas’s style takes padding and justifyContent, so you can push content to the bottom or spread it out ("space-between").
render() writes a Canvas as HTML too, to look at in a browser while you design it.
renderImage()
renderImage(element, options) returns { image, width, height, diagnostics }: the file’s bytes, its size in pixels, and the warnings and notes. Errors reject with an ElementsError listing them. The same template writes the same bytes.
| Option | What it takes |
|---|---|
fonts |
Font files, by family (below) |
images |
Image files, by URL (below) |
format |
"png" (the default) or "svg". An SVG’s text is drawn as paths, so it shows without the fonts |
scale |
Pixels per px of a PNG: 2 for a phone’s screen. At 2, a 1200 by 630 Canvas is 2400 by 1260 |
const { image } = await renderImage(<PostCard title="Launch week" author="Ada Lovelace" />, { fonts, images, format: "svg" });
await writeFile("card.svg", image);
Fonts and images
Nothing is fetched. Give each file the template needs, as bytes:
- Fonts in
fonts:{ family, src, weight?, style? }, one entry per file.srcis a TTF, OTF or WOFF file’s bytes, andfamilyis the name yourfontFamilystacks use. A Canvas’s ownfontsgiven asdata:URIs are drawn too. Ones given by URL aren’t loaded. - Images in
images, by the URL the template gives them ({ "https://…/photo.png": bytes }), or asdata:URIs in the template: PNG, JPEG, GIF or SVG.
An image with text needs at least one font. Text whose fontFamily has no file given is drawn in the first font given.
Diagnostics
| Code | Severity | When |
|---|---|---|
image/root |
error | The template isn’t a Canvas |
image/version |
error | A second copy of @hyper-elements/react lays the template out in another version |
image/font |
error | The Canvas has text and no font is given |
image/font |
warning | A font in the Canvas’s fonts isn’t given, or a file given is WOFF2 |
image/font-weight |
warning | Text in a weight or style of a family that isn’t given. It’s drawn in the nearest file given |
image/image |
error | An image isn’t given, or its file isn’t a PNG, JPEG, GIF or SVG |
image/unsupported |
warning | What an image can’t show that the HTML can: a hover, a Video’s player, an Html block’s markup (its plain text is drawn), a line too wide to wrap (it goes past the edge) |
canvas/overflow |
warning | The content looks taller than the Canvas. An estimate: text isn’t measured yet |
validate() from @hyper-elements/react reports image/unsupported and canvas/overflow without rendering. The font and image checks need the files, so only renderImage() makes them.
Limits
- WOFF2 isn’t drawn. Satori reads TTF, OTF and WOFF. Give one of those.
- Text is drawn only in the fonts you give. A character none of them has comes out as an empty box. Give a font that has it.
- What doesn’t fit is cut off at the Canvas’s edges. Text isn’t measured before layout, so check long titles:
canvas/overflowis an estimate. - No hover and no video. A Button shows its own colors, a Video its title.