ImageResponse
Renders JSX and a CSS subset to a PNG on the server for Open Graph and social card images.
new ImageResponse(element: ReactElement, options: { width?: number = 1200, height?: number = 630, emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji', fonts?: { name: string, data: ArrayBuffer, weight: number, style: 'normal' | 'italic' }[], debug?: boolean = false, status?: number = 200, statusText?: string, headers?: Record<string, string> })- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$element | ReactElement | The JSX to rasterise. Only flexbox and a subset of CSS properties are supported. |
$options.widthoptional | number= 1200 | Output width in pixels. |
$options.heightoptional | number= 630 | Output height in pixels. |
$options.emojioptional | 'twemoji' | 'blobmoji' | 'noto' | 'openmoji'= 'twemoji' | Which emoji set to substitute when rendering emoji characters. |
$options.fontsoptional | { name: string, data: ArrayBuffer, weight: number, style: 'normal' | 'italic' }[] | Custom fonts to embed. Only ttf, otf and woff are supported; ttf or otf parse fastest. |
$options.debugoptional | boolean= false | Draw layout guides over the rendered image. |
$options.statusoptional | number= 200 | HTTP status code passed through to the response. |
$options.statusTextoptional | string | HTTP status text passed through to the response. |
$options.headersoptional | Record<string, string> | Extra headers passed through to the response. |
Return value
ImageResponseA Response whose body is the rendered PNG, returnable directly from a Route Handler or a metadata image file.
Errors
Rendering fails when the JSX uses unsupported CSS such as display: grid, when an element has multiple children without an explicit display: flex, or when the bundle including JSX, CSS, fonts and images exceeds 500KB.
Changelog
- PHP v14.0.0ImageResponse moved from next/server to next/og. Update the import path when upgrading.
- PHP v13.3.0ImageResponse can be imported from next/server.
- PHP v13.0.0ImageResponse introduced via the @vercel/og package.
ImageResponse rasterises JSX into a PNG on the server using Satori and Resvg. It is not a browser: there is no layout engine, no JavaScript in the output, and no way to hydrate it. You get a picture.
When to reach for it
Almost always for social preview images. Export it as the default from app/opengraph-image.tsx (or the twitter-image equivalent) and Next.js wires up the metadata tags and content type for you. Use a Route Handler instead only when you need an image at an arbitrary URL with your own query parameters.
What trips people up
- Flexbox only.
display: griddoes nothing, and any element with more than one child needs an explicitdisplay: 'flex'or rendering throws. - The whole bundle — JSX, CSS, fonts, images — is capped at 500KB. A single unsubsetted font can eat most of it.
- Fonts must be
ttf,otforwoff;woff2is not supported. Read font files once at module scope, not per request. - No Tailwind classes unless you configure the
twprop; write inlinestyleobjects. - It is server-only, and long text will not wrap sensibly without explicit widths.
Example
import { ImageResponse } from 'next/og'
// Image metadata
export const alt = 'My site'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
// Image generation
export default async function Image() {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
My site
</div>
),
{
// Re-use the exported size config for the ImageResponse
...size,
}
)
}When to use it
- Generate a per-post Open Graph image from the post title
- Serve a branded fallback social card for the site root
- Render a dynamic image at a custom URL from a Route Handler
- Embed a subsetted brand font in generated share cards
Frequently asked questions
Why does my ImageResponse throw about display: flex?
Satori only supports flexbox, and any element with more than one child must set display: 'flex' explicitly — there is no block layout fallback. display: grid and other advanced layouts do not work at all. Add display: 'flex' to every container in the tree.
Where do I import ImageResponse from?
From next/og since v14.0.0. It was importable from next/server in v13.3.0 and originally shipped as the @vercel/og package in v13.0.0, so older tutorials show the wrong path.
Can I use Tailwind classes in an OG image?
Not through className. Satori accepts a tw prop for a subset of Tailwind, but the reliable approach is inline style objects with flexbox properties, since that is what the renderer supports directly.
More examples
A per-post card in a dynamic route
params became a promise for metadata image files in Next.js 16.0.0 (in 15 it was still a plain object), so it is awaited before fetching the post. Every wrapper carries an explicit display: 'flex' — including the two leaf divs — because Satori throws on any element with multiple children that has no flex display, and adding it everywhere is cheaper than debugging which node tripped it.
import { ImageResponse } from 'next/og'
export const alt = 'Blog post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background: '#0b0b0c',
color: 'white',
padding: 64,
}}
>
<div style={{ display: 'flex', fontSize: 28, opacity: 0.7 }}>
example.com
</div>
<div style={{ display: 'flex', fontSize: 68, lineHeight: 1.1 }}>
{post.title}
</div>
</div>
),
{ ...size }
)
}Embedding a custom font
The top-level await runs once when the module is evaluated, not per image, which keeps the read off the request path. Only ttf, otf and woff parse — woff2 fails — and the font counts against the 500KB bundle cap, so subset it to the characters you actually render if the file is large.
import { ImageResponse } from 'next/og'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
export const alt = 'My site'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
// Read once at module scope — the font does not depend on request data
const interSemiBold = await readFile(
join(process.cwd(), 'assets/Inter-SemiBold.ttf')
)
export default async function Image() {
return new ImageResponse(
(
<div style={{ display: 'flex', fontFamily: 'Inter', fontSize: 72 }}>
Hello
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: interSemiBold,
style: 'normal',
weight: 400,
},
],
}
)
}Serving an image from a Route Handler
A Route Handler gives the image its own URL and query parameters, which is what you want when something outside the app — an email template, a docs site — needs to request a card. The try/catch matters here: an unsupported CSS property throws at render time, and without it the consumer gets an opaque 500 with no clue which style caused it.
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') ?? 'Untitled'
try {
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
fontSize: 60,
padding: 40,
}}
>
{title}
</div>
),
{ width: 1200, height: 630 }
)
} catch (e) {
return new Response('Failed to generate the image', { status: 500 })
}
}
Discussion