ImageResponse

Renders JSX and a CSS subset to a PNG on the server for Open Graph and social card images.

Signature
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

ParameterTypeDescription
$elementReactElementThe JSX to rasterise. Only flexbox and a subset of CSS properties are supported.
$options.widthoptionalnumber= 1200Output width in pixels.
$options.heightoptionalnumber= 630Output 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.debugoptionalboolean= falseDraw layout guides over the rendered image.
$options.statusoptionalnumber= 200HTTP status code passed through to the response.
$options.statusTextoptionalstringHTTP status text passed through to the response.
$options.headersoptionalRecord<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: grid does nothing, and any element with more than one child needs an explicit display: '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, otf or woff; woff2 is not supported. Read font files once at module scope, not per request.
  • No Tailwind classes unless you configure the tw prop; write inline style objects.
  • It is server-only, and long text will not wrap sensibly without explicit widths.

Example

Example · typescript
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.

Example · typescript
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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.