Skip to main content
OrchestKit v7.85.0 — 107 skills, 37 agents, 188 hooks · Claude Code 2.1.132+
OrchestKit
Skills

Multi Surface Render

Multi-surface rendering with json-render — same JSON spec produces React web, Next.js apps, React Native, Ink terminal UIs, PDFs, emails, Remotion videos, OG images, and 3D scenes. Covers renderer target selection, registry mapping, and platform-specific APIs (renderToBuffer, renderToStream, renderToFile). Use when generating output for multiple platforms, creating PDF reports, email templates, demo videos, or social media images from a single component spec.

Reference medium

Auto-activated — this skill loads automatically when Claude detects matching context.

Multi-Surface Rendering with json-render

Define once, render everywhere. A single json-render catalog and spec can produce React web UIs, PDF reports, HTML emails, Remotion demo videos, and OG images — each surface gets its own registry that maps catalog types to platform-native components.

Quick Reference

CategoryRulesImpactWhen to Use
Target Selection1HIGHChoosing which renderer for your use case
React Renderer1MEDIUMWeb apps, SPAs, dashboards
PDF & Email Renderer1HIGHReports, documents, notifications
Video & Image Renderer1MEDIUMDemo videos, OG images, social cards
Registry Mapping1HIGHPlatform-specific component implementations

Total: 5 rules across 5 categories

How Multi-Surface Rendering Works

  1. One catalog — Zod-typed component definitions shared across all surfaces
  2. One spec — flat-tree JSON/YAML describing the UI structure
  3. Many registries — each surface maps catalog types to its own component implementations
  4. Many renderers — each package renders the spec using its registry

The catalog is the contract. The spec is the data. The registry is the platform-specific implementation.

Quick Start — Same Catalog, Different Renderers

Shared Catalog (used by all surfaces)

import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Heading: {
    props: z.object({
      text: z.string(),
      level: z.enum(['h1', 'h2', 'h3']),
    }),
    children: false,
  },
  Paragraph: {
    props: z.object({ text: z.string() }),
    children: false,
  },
  StatCard: {
    props: z.object({
      label: z.string(),
      value: z.string(),
      trend: z.enum(['up', 'down', 'flat']).optional(),
    }),
    children: false,
  },
})

Render to Web (React)

import { Renderer } from '@json-render/react'
import { catalog } from './catalog'
import { webRegistry } from './registries/web'

export const Dashboard = ({ spec }) => (
  <Renderer spec={spec} catalog={catalog} registry={webRegistry} />
)

Render to PDF

import { renderToBuffer, renderToFile } from '@json-render/react-pdf'
import { catalog } from './catalog'
import { pdfRegistry } from './registries/pdf'

// Buffer for HTTP response
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })

// Direct file output
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })

Render to Email

import { renderToHtml } from '@json-render/react-email'
import { catalog } from './catalog'
import { emailRegistry } from './registries/email'

const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
await sendEmail({ to: user.email, subject: 'Weekly Report', html })

Render to OG Image (Satori)

import { renderToSvg, renderToPng } from '@json-render/image'
import { catalog } from './catalog'
import { imageRegistry } from './registries/image'

const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

Render to Video (Remotion)

import { JsonRenderComposition } from '@json-render/remotion'
import { catalog } from './catalog'
import { remotionRegistry } from './registries/remotion'

export const DemoVideo = () => (
  <JsonRenderComposition
    spec={spec}
    catalog={catalog}
    registry={remotionRegistry}
    fps={30}
    durationInFrames={150}
  />
)

Render to Terminal (Ink, 0.15+)

import { render } from 'ink'
import { InkRenderer } from '@json-render/ink'
import { catalog } from './catalog'
import { inkRegistry } from './registries/ink'

render(<InkRenderer spec={spec} catalog={catalog} registry={inkRegistry} />)

Useful for /ork:* CLI dashboards and streaming agent chat interfaces — ships 20+ Ink-native components (Box, Text, Spinner, Table, Markdown, Progress, etc.).

Render to Next.js App (0.16+)

import { generateNextApp } from '@json-render/next'

await generateNextApp(spec, {
  catalog,
  registry: webRegistry,
  outDir: './out',
  // generates routes, layouts, SSR handlers, and metadata
})

Output is a full Next.js App Router project — specs describe route trees, not just components.

Decision Matrix — When to Use Each Target

TargetPackageWhen to UseOutput
React@json-render/reactWeb apps, SPAsJSX
Next.js@json-render/next (0.16+)Full apps: routes, layouts, SSR, metadataNext.js app
Vue@json-render/vueVue projectsVue components
Svelte@json-render/svelteSvelte projectsSvelte components
Svelte+shadcn@json-render/shadcn-svelte (0.16+)36-component Svelte 5 catalogSvelte + Tailwind
React Native@json-render/react-nativeMobile apps (25+ components)Native views
Terminal@json-render/ink (0.15+)CLI UIs, TUIs, streaming chatInk (terminal)
PDF@json-render/react-pdfReports, documentsPDF buffer/file
Email@json-render/react-emailNotifications, digestsHTML string
Remotion@json-render/remotionDemo videos, marketingMP4/WebM
Image@json-render/imageOG images, social cardsSVG/PNG (Satori)
YAML@json-render/yaml (0.14+)Token optimization, streaming parserYAML string
MCP@json-render/mcpClaude/Cursor/ChatGPT conversationsSandboxed iframe
3D@json-render/react-three-fiber3D scenes (20 components, incl. GaussianSplat in 0.17)Three.js canvas
Codegen@json-render/codegenSource code from specsTypeScript/JSX

Load rules/target-selection.md for detailed selection criteria and trade-offs.

PDF Renderer — Reports and Documents

The @json-render/react-pdf package renders specs to PDF using react-pdf under the hood. Three output modes: buffer, file, and stream.

import { renderToBuffer, renderToFile, renderToStream } from '@json-render/react-pdf'

// In-memory buffer (for HTTP responses, S3 upload)
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
res.send(buffer)

// Direct file write
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })

// Streaming (for large documents)
const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
stream.pipe(res)

Load rules/pdf-email-renderer.md for PDF registry patterns and email rendering.

Image Renderer — OG Images and Social Cards

The @json-render/image package uses Satori to convert specs to SVG, then optionally to PNG. Designed for server-side generation of social media images.

import { renderToSvg, renderToPng } from '@json-render/image'

// SVG output (smaller, scalable)
const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

// PNG output (universal compatibility)
const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

Load rules/video-image-renderer.md for Satori constraints and Remotion composition patterns.

Registry Mapping — Same Catalog, Platform-Specific Components

Each surface needs its own registry. The registry maps catalog types to platform-specific component implementations while the catalog and spec stay identical.

// Web registry — uses HTML elements
const webRegistry = {
  Heading: ({ text, level }) => {
    const Tag = level // h1, h2, h3
    return <Tag className="font-bold">{text}</Tag>
  },
  StatCard: ({ label, value, trend }) => (
    <div className="rounded border p-4">
      <span className="text-sm text-gray-500">{label}</span>
      <strong className="text-2xl">{value}</strong>
    </div>
  ),
}

// PDF registry — uses react-pdf primitives
import { Text, View } from '@react-pdf/renderer'
const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ fontSize: level === 'h1' ? 24 : level === 'h2' ? 18 : 14 }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={{ border: '1pt solid #ccc', padding: 8 }}>
      <Text style={{ fontSize: 10, color: '#666' }}>{label}</Text>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{value}</Text>
    </View>
  ),
}

Load rules/registry-mapping.md for registry creation patterns and type safety.

Rule Details

Target Selection

Decision criteria for choosing the right renderer target.

RuleFileKey Pattern
Target Selectionrules/target-selection.mdUse case mapping, output format constraints

React Renderer

Web rendering with the &lt;Renderer&gt; component.

RuleFileKey Pattern
React Rendererrules/react-renderer.md&lt;Renderer&gt; component, streaming, error boundaries

PDF & Email Renderer

Server-side rendering to PDF buffers/files and HTML email strings.

RuleFileKey Pattern
PDF & Emailrules/pdf-email-renderer.mdrenderToBuffer, renderToFile, renderToHtml

Video & Image Renderer

Remotion compositions and Satori image generation.

RuleFileKey Pattern
Video & Imagerules/video-image-renderer.mdJsonRenderComposition, renderToPng, renderToSvg

Registry Mapping

Creating platform-specific registries for a shared catalog.

RuleFileKey Pattern
Registry Mappingrules/registry-mapping.mdPer-platform registries, type-safe mapping

Key Decisions

DecisionRecommendation
PDF libraryUse @json-render/react-pdf (react-pdf), not Puppeteer screenshots
Email renderingUse @json-render/react-email (react-email), not MJML or custom HTML
OG imagesUse @json-render/image (Satori), not Puppeteer or canvas
VideoUse @json-render/remotion (Remotion), not FFmpeg scripts
Registry per platformAlways separate registries; never one registry for all surfaces
Catalog sharingOne catalog definition shared via import across all registries

Common Mistakes

  1. Building separate component trees for each surface — defeats the purpose; share the catalog and spec
  2. Using Puppeteer to screenshot React for PDF generation — slow, fragile; use native react-pdf rendering
  3. One giant registry covering all platforms — impossible since PDF uses &lt;View&gt;/&lt;Text&gt;, web uses <div>/<span>
  4. Forgetting Satori limitations — no CSS grid, limited flexbox; design image registries with these constraints
  5. Duplicating catalog definitions per surface — one catalog, many registries; the catalog is the contract
  • ork:json-render-catalog — Catalog definition patterns with Zod, shadcn components
  • ork:demo-producer — Video production pipeline using Remotion
  • ork:presentation-builder — Slide deck generation
  • ork:mcp-visual-output — Rendering specs in Claude/Cursor via MCP

Rules (5)

Use native react-pdf and react-email renderers instead of browser-based workarounds — HIGH

PDF & Email Renderer

@json-render/react-pdf renders specs to PDF using react-pdf primitives (View, Text, Image). @json-render/react-email renders specs to HTML email strings using react-email components. Both validate against the same catalog.

Incorrect — using Puppeteer to screenshot React for PDF:

// WRONG: Launches a browser, takes a screenshot, converts to PDF
import puppeteer from 'puppeteer'

async function generatePdf(spec) {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.setContent(renderToString(<Dashboard spec={spec} />))
  const pdf = await page.pdf({ format: 'A4' })
  await browser.close()
  return pdf // slow, ~2s startup, CSS rendering differences, no catalog validation
}

Correct — native PDF rendering via react-pdf:

import { renderToBuffer, renderToFile, renderToStream } from '@json-render/react-pdf'
import { catalog } from './catalog'
import { pdfRegistry } from './registries/pdf'

// Buffer — for HTTP responses, S3 upload, attachments
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })

// File — direct disk write
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })

// Stream — for large documents, pipe to HTTP response
const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
stream.pipe(res)

Incorrect — manual HTML string for email:

// WRONG: Manual HTML, no validation, rendering inconsistencies
const html = `<table><tr><td>${data.title}</td></tr></table>`

Correct — react-email rendering:

import { renderToHtml } from '@json-render/react-email'
import { catalog } from './catalog'
import { emailRegistry } from './registries/email'

const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
await transporter.sendMail({ to: user.email, subject: 'Report', html })

Key rules:

  • Use renderToBuffer for in-memory PDF (HTTP responses, email attachments, cloud storage upload)
  • Use renderToFile for disk output (batch report generation, CI artifacts)
  • Use renderToStream for large documents to avoid buffering the entire PDF in memory
  • PDF registry components must use react-pdf primitives (View, Text, Image) — not HTML elements
  • Email registry components must use react-email primitives (Section, Text, Heading) — not arbitrary HTML
  • Both renderers validate specs against the catalog — invalid types or props throw at render time

PDF Registry Pattern

import { View, Text, StyleSheet } from '@react-pdf/renderer'

const styles = StyleSheet.create({
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  card: { border: '1pt solid #e5e7eb', padding: 12, borderRadius: 4 },
})

export const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ ...styles.heading, fontSize: level === 'h1' ? 24 : level === 'h2' ? 18 : 14 }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={styles.card}>
      <Text style={{ fontSize: 10, color: '#6b7280' }}>{label}</Text>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{value}</Text>
    </View>
  ),
}

Use the Renderer component with catalog validation for web rendering — MEDIUM

React Renderer

The &lt;Renderer&gt; component from @json-render/react validates specs against the catalog at runtime and renders each element using the registry. Never parse specs manually.

Incorrect — parsing the spec manually:

// WRONG: Manual parsing, no validation, no streaming
function Dashboard({ spec }) {
  return (
    <div>
      {Object.entries(spec.elements).map(([id, el]) => {
        const Component = components[el.type] // no catalog validation
        return <Component key={id} {...el.props} />
      })}
    </div>
  )
}

Correct — using the Renderer component:

import { Renderer } from '@json-render/react'
import { catalog } from './catalog'
import { webRegistry } from './registries/web'

function Dashboard({ spec }) {
  return (
    <Renderer
      spec={spec}
      catalog={catalog}
      registry={webRegistry}
      fallback={<LoadingSkeleton />}
      onError={(err) => console.error('Render error:', err)}
    />
  )
}

Key rules:

  • Always pass catalog to &lt;Renderer&gt; — it validates that spec types exist in the catalog and props match Zod schemas
  • Use fallback prop for loading states during progressive streaming
  • Use onError callback or wrap in an error boundary for graceful degradation
  • For streaming specs (AI generating in real-time), the Renderer updates progressively as elements arrive
  • The registry maps catalog types to React components — keep it separate from the catalog definition

Progressive Streaming Pattern

import { Renderer, useStreamingSpec } from '@json-render/react'

function StreamingDashboard({ specStream }) {
  const spec = useStreamingSpec(specStream) // updates as patches arrive

  return (
    <Renderer
      spec={spec}
      catalog={catalog}
      registry={webRegistry}
      fallback={<Skeleton />}
    />
  )
}

Elements render as soon as their props are complete — the user sees the UI building in real-time.

Create separate registries per platform sharing a single catalog — HIGH

Registry Mapping

A registry maps each catalog type to a platform-specific component implementation. The catalog (Zod schemas) and spec (flat-tree data) stay identical across surfaces. Only the registry changes.

Incorrect — one giant registry trying to cover all platforms:

// WRONG: Impossible — PDF needs View/Text, web needs div/span
const universalRegistry = {
  Heading: ({ text, level, platform }) => {
    if (platform === 'pdf') return <Text style={...}>{text}</Text>
    if (platform === 'email') return <Heading as={level}>{text}</Heading>
    return <h1>{text}</h1> // web fallback
  },
}

Correct — separate registries per platform, same catalog:

import { catalog } from './catalog' // SHARED — one definition

// Web registry
export const webRegistry = {
  Heading: ({ text, level }) => {
    const Tag = level
    return <Tag className="font-bold tracking-tight">{text}</Tag>
  },
  StatCard: ({ label, value, trend }) => (
    <div className="rounded-lg border p-4 shadow-sm">
      <p className="text-sm text-muted-foreground">{label}</p>
      <p className="text-2xl font-bold">{value}</p>
      {trend && <TrendIcon direction={trend} />}
    </div>
  ),
}

// PDF registry
import { View, Text } from '@react-pdf/renderer'
export const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ fontSize: level === 'h1' ? 24 : 18, fontWeight: 'bold' }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={{ border: '1pt solid #ccc', padding: 8 }}>
      <Text style={{ fontSize: 10, color: '#666' }}>{label}</Text>
      <Text style={{ fontSize: 18 }}>{value}</Text>
    </View>
  ),
}

// Email registry
import { Section, Text as EmailText, Heading as EmailHeading } from '@react-email/components'
export const emailRegistry = {
  Heading: ({ text, level }) => (
    <EmailHeading as={level}>{text}</EmailHeading>
  ),
  StatCard: ({ label, value }) => (
    <Section style={{ border: '1px solid #e5e7eb', padding: '12px' }}>
      <EmailText style={{ fontSize: '12px', color: '#6b7280' }}>{label}</EmailText>
      <EmailText style={{ fontSize: '20px', fontWeight: 'bold' }}>{value}</EmailText>
    </Section>
  ),
}

Key rules:

  • One catalog, many registries — the catalog defines WHAT can be rendered, registries define HOW
  • Every catalog type must have an entry in each registry — missing entries throw at render time
  • Registry components receive the same props defined in the catalog Zod schema
  • Never add platform-specific props to the catalog — the catalog is platform-agnostic
  • Organize registries in ./registries/web.ts, ./registries/pdf.ts, ./registries/email.ts
  • Use CatalogComponents&lt;typeof catalog&gt; type to ensure registries match the catalog

Type-Safe Registry Pattern

import type { CatalogComponents } from '@json-render/react'
import type { catalog } from './catalog'

// TypeScript ensures every catalog type is implemented
export const webRegistry: CatalogComponents<typeof catalog> = {
  Heading: ({ text, level }) => { /* ... */ },
  StatCard: ({ label, value, trend }) => { /* ... */ },
  // Missing type → TypeScript error
}

File Organization

src/
  catalog.ts              # Shared catalog (Zod schemas)
  registries/
    web.ts                # React/HTML components
    pdf.ts                # react-pdf View/Text components
    email.ts              # react-email Section/Text components
    image.ts              # Satori-compatible inline-style components
    remotion.ts           # Remotion-animated components

Select renderer target based on output format and platform constraints — HIGH

Target Selection

Choose the renderer target based on what the output is, not what framework you use. Each target maps to a specific @json-render/* package with its own rendering pipeline.

Incorrect — building separate templates for each surface:

// WRONG: Separate template systems, no shared catalog
const webDashboard = buildReactComponents(data)
const pdfReport = buildPdfWithPuppeteer(data)     // puppeteer screenshot
const emailDigest = buildMjmlEmail(data)           // separate MJML templates
const ogImage = buildCanvasImage(data)             // manual canvas drawing

Correct — one catalog, one spec, multiple registries:

import { catalog } from './catalog'

// Same spec, different renderers
import { Renderer } from '@json-render/react'           // web
import { renderToBuffer } from '@json-render/react-pdf' // pdf
import { renderToHtml } from '@json-render/react-email' // email
import { renderToPng } from '@json-render/image'        // og image

// Each renderer uses the same catalog + spec, different registry
const webUi = <Renderer spec={spec} catalog={catalog} registry={webRegistry} />
const pdf = await renderToBuffer(spec, { catalog, registry: pdfRegistry })
const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
const png = await renderToPng(spec, { catalog, registry: imageRegistry, width: 1200, height: 630 })

Key rules:

  • Match target to output format: PDF document = react-pdf, HTML email = react-email, image = image
  • Never use Puppeteer/Playwright to screenshot a React page for PDF — use native @json-render/react-pdf
  • Never build custom MJML/HTML templates when @json-render/react-email exists
  • If output is a file (PDF, PNG, MP4), use the server-side renderer — not the React &lt;Renderer&gt; component
  • Multiple targets in one project is the normal case — that is the entire point of json-render

Selection Checklist

NeedTargetPackage
Interactive web UIReact@json-render/react
Downloadable documentPDF@json-render/react-pdf
Transactional emailEmail@json-render/react-email
Social preview cardImage@json-render/image
Marketing videoVideo@json-render/remotion
Mobile app screenReact Native@json-render/react-native
AI conversation outputMCP@json-render/mcp
Source code generationCodegen@json-render/codegen

Use Remotion compositions and Satori for video and image generation from specs — MEDIUM

Video & Image Renderer

@json-render/remotion wraps specs into Remotion compositions for MP4/WebM video. @json-render/image uses Satori to render specs as SVG, then optionally converts to PNG for OG images and social cards.

Incorrect — manually creating Remotion timelines:

// WRONG: Manual timeline, no catalog validation, duplicated rendering logic
export const MyVideo = () => (
  <Composition
    id="demo"
    component={() => (
      <div>
        <h1>{data.title}</h1>
        <p>{data.description}</p>
      </div>
    )}
    durationInFrames={150}
    fps={30}
    width={1920}
    height={1080}
  />
)

Correct — JsonRenderComposition from spec:

import { JsonRenderComposition } from '@json-render/remotion'
import { catalog } from './catalog'
import { remotionRegistry } from './registries/remotion'

export const DemoVideo = () => (
  <JsonRenderComposition
    spec={spec}
    catalog={catalog}
    registry={remotionRegistry}
    fps={30}
    durationInFrames={150}
    width={1920}
    height={1080}
  />
)

Incorrect — using Puppeteer for OG images:

// WRONG: Launches browser, screenshots a page, saves as PNG
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setViewport({ width: 1200, height: 630 })
await page.setContent(`<div style="...">${title}</div>`)
const png = await page.screenshot({ type: 'png' })

Correct — Satori-based image rendering:

import { renderToSvg, renderToPng } from '@json-render/image'
import { catalog } from './catalog'
import { imageRegistry } from './registries/image'

// SVG (smaller file size, scalable)
const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

// PNG (universal compatibility)
const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

Key rules:

  • Use JsonRenderComposition for Remotion videos — it validates specs against the catalog
  • Use renderToPng for OG images (1200x630 standard) — Satori is server-side, no browser needed
  • Satori has CSS limitations: no CSS grid, limited flexbox, no position: absolute nesting — design image registries accordingly
  • Image registries must use inline styles only — Satori does not support className or CSS files
  • For Remotion, the registry can use Remotion animation primitives (useCurrentFrame, interpolate, spring)

Satori CSS Constraints

SupportedNot Supported
Flexbox (basic)CSS Grid
border, borderRadiusbox-shadow
padding, marginposition: absolute (limited)
fontSize, fontWeightExternal CSS, className
color, backgroundColorCSS animations
width, heightMedia queries

Common OG Image Dimensions

PlatformWidthHeightRatio
Open Graph (general)12006301.91:1
Twitter card12006281.91:1
LinkedIn share12006271.91:1
Facebook share12006301.91:1

References (6)

Renderer Api

Renderer API Reference

@json-render/react

&lt;Renderer&gt; Component

import { Renderer } from '@json-render/react'

<Renderer
  spec={spec}                    // JsonRenderSpec — flat-tree JSON/YAML
  catalog={catalog}              // Catalog from defineCatalog()
  registry={registry}            // CatalogComponents<typeof catalog>
  fallback={<Loading />}         // Optional: shown during streaming
  onError={(err) => log(err)}    // Optional: error callback
/>

useStreamingSpec Hook

import { useStreamingSpec } from '@json-render/react'

const spec = useStreamingSpec(stream) // ReadableStream of JSON Patch ops

@json-render/react-pdf

renderToBuffer(spec, options): Promise&lt;Buffer&gt;

Renders spec to an in-memory PDF buffer.

import { renderToBuffer } from '@json-render/react-pdf'

const buffer = await renderToBuffer(spec, {
  catalog,
  registry: pdfRegistry,
  pageSize: 'A4',               // Optional: 'A4' | 'LETTER' | { width, height }
  orientation: 'portrait',      // Optional: 'portrait' | 'landscape'
  margins: { top: 40, bottom: 40, left: 40, right: 40 }, // Optional
})

renderToFile(spec, path, options): Promise&lt;void&gt;

Renders spec directly to a PDF file on disk.

import { renderToFile } from '@json-render/react-pdf'

await renderToFile(spec, './output/report.pdf', {
  catalog,
  registry: pdfRegistry,
  pageSize: 'A4',
})

renderToStream(spec, options): Promise&lt;ReadableStream&gt;

Renders spec to a readable stream for piping to HTTP responses.

import { renderToStream } from '@json-render/react-pdf'

const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"')
stream.pipe(res)

@json-render/react-email

renderToHtml(spec, options): Promise&lt;string&gt;

Renders spec to an HTML string optimized for email clients.

import { renderToHtml } from '@json-render/react-email'

const html = await renderToHtml(spec, {
  catalog,
  registry: emailRegistry,
  preview: 'Your weekly report is ready', // Optional: email preview text
  theme: 'light',                          // Optional: 'light' | 'dark'
})

renderToPlainText(spec, options): Promise&lt;string&gt;

Renders spec to plain text (for text/plain multipart emails).

import { renderToPlainText } from '@json-render/react-email'

const text = await renderToPlainText(spec, { catalog, registry: emailRegistry })

@json-render/image

renderToSvg(spec, options): Promise&lt;string&gt;

Renders spec to an SVG string using Satori.

import { renderToSvg } from '@json-render/image'

const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,              // Required
  height: 630,              // Required
  fonts: [{                 // Optional: custom fonts
    name: 'Inter',
    data: fontBuffer,
    weight: 400,
    style: 'normal',
  }],
})

renderToPng(spec, options): Promise&lt;Buffer&gt;

Renders spec to a PNG buffer (SVG -> PNG via Resvg).

import { renderToPng } from '@json-render/image'

const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

@json-render/remotion

&lt;JsonRenderComposition&gt; Component

import { JsonRenderComposition } from '@json-render/remotion'

<JsonRenderComposition
  spec={spec}                    // JsonRenderSpec
  catalog={catalog}              // Catalog from defineCatalog()
  registry={remotionRegistry}    // Registry with Remotion animations
  fps={30}                       // Frames per second
  durationInFrames={150}         // Total duration
  width={1920}                   // Video width
  height={1080}                  // Video height
/>

Remotion Registry with Animations

import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion'

const remotionRegistry = {
  Heading: ({ text, level }) => {
    const frame = useCurrentFrame()
    const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' })
    return <h1 style={{ opacity, fontSize: level === 'h1' ? 48 : 36 }}>{text}</h1>
  },
}

Common Options (all renderers)

OptionTypeDescription
catalogCatalogRequired. Zod-typed component definitions
registryCatalogComponentsRequired. Platform-specific component map
specJsonRenderSpecRequired. Flat-tree JSON/YAML spec
onError(err: Error) => voidOptional. Error handler

Target Comparison

Target Comparison Matrix

All Targets — Capabilities Overview

TargetPackageOutputServer-SideStreamingFile OutputSizing
React@json-render/reactJSXNo (client)Yes (progressive)NoResponsive
Vue@json-render/vueVue componentsSSR supportedYesNoResponsive
Svelte@json-render/svelteSvelte componentsSSR supportedYesNoResponsive
React Native@json-render/react-nativeNative viewsNoYesNoFlex-based
PDF@json-render/react-pdfBuffer/File/StreamYesVia streamYesFixed (A4, Letter)
Email@json-render/react-emailHTML stringYesNoNo600px max-width
Remotion@json-render/remotionMP4/WebMYes (render)NoYesFixed (px)
Image@json-render/imageSVG/PNGYesNoYes (buffer)Fixed (px)
YAML@json-render/yamlYAML stringYesNoNoN/A
MCP@json-render/mcpSandboxed iframeYesYesNoConstrained
3D@json-render/react-three-fiberThree.js canvasNoNoNoCanvas-based
Codegen@json-render/codegenTypeScript/JSXYesNoYesN/A

Registry Component Primitives by Target

TargetBase ElementsStylingLayout
Reactdiv, span, h1-h6, p, imgclassName, CSS modules, TailwindFlexbox, Grid, any CSS
PDFView, Text, Image (react-pdf)StyleSheet.create()Flexbox only
EmailSection, Text, Heading, Button (react-email)Inline stylesTable-based (email compat)
Imagediv, span, img (Satori subset)Inline styles onlyFlexbox only (limited)
Remotiondiv, span + Remotion primitivesInline styles, CSSFlexbox, absolute positioning
React NativeView, Text, Image (RN)StyleSheet.create()Flexbox only

Limitations by Target

PDF (@json-render/react-pdf)

  • No CSS grid — flexbox only
  • No className — use StyleSheet.create()
  • No <div>/<span> — must use &lt;View&gt;/&lt;Text&gt;
  • Font embedding required for custom fonts
  • No interactive elements (buttons, links are display-only)

Email (@json-render/react-email)

  • 600px max-width (email client constraint)
  • Table-based layout for Outlook compatibility
  • Limited CSS support (no flexbox in Outlook, no grid)
  • Inline styles only for maximum compatibility
  • No JavaScript interactivity

Image (@json-render/image / Satori)

  • No CSS grid
  • Limited flexbox (no flex-wrap, limited align-items)
  • No box-shadow
  • No CSS animations or transitions
  • No external stylesheets — inline styles only
  • position: absolute has limited nesting support
  • Custom fonts must be loaded as ArrayBuffer

Remotion (@json-render/remotion)

  • No user interaction (pre-rendered video)
  • Rendering is CPU-intensive — use cloud rendering for production
  • Must specify exact durationInFrames and fps
  • All animations must be frame-based (useCurrentFrame)

React Native (@json-render/react-native)

  • No div/span — use View/Text
  • No CSS grid — flexbox only
  • No className — use StyleSheet.create()
  • Platform-specific behavior (iOS vs Android)

Performance Characteristics

TargetRender TimeMemoryCPU
React<50ms (client)LowLow
PDF (buffer)200-500msMediumMedium
PDF (stream)100-300ms startLowMedium
Email50-100msLowLow
Image (SVG)100-200msLowLow
Image (PNG)200-400msMediumMedium
Remotion10-60s (full render)HighHigh
Codegen50-100msLowLow

When to Use Multiple Targets

Common multi-target combinations:

Use CaseTargetsExample
Dashboard + PDF exportReact + PDFWeb dashboard with "Download PDF" button
Dashboard + email digestReact + EmailWeb view + weekly email summary
Blog + social sharingReact + ImageBlog post + OG image preview
Product page + demoReact + RemotionLanding page + demo video
Full marketing suiteReact + PDF + Email + Image + RemotionAll surfaces from one spec

Upstream Email

<!-- SYNCED from vercel-labs/json-render (skills/react-email/SKILL.md) --> <!-- Hash: 8b376b37da07dd68944e72e0e322a272571f39271e5b08b6efd437db201470c9 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-email

React Email renderer that converts JSON specs into HTML or plain-text email output.

Quick Start

import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";

const catalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

const spec = {
  root: "html-1",
  elements: {
    "html-1": { type: "Html", props: { lang: "en", dir: "ltr" }, children: ["head-1", "body-1"] },
    "head-1": { type: "Head", props: {}, children: [] },
    "body-1": {
      type: "Body",
      props: { style: { backgroundColor: "#f6f9fc" } },
      children: ["container-1"],
    },
    "container-1": {
      type: "Container",
      props: { style: { maxWidth: "600px", margin: "0 auto", padding: "20px" } },
      children: ["heading-1", "text-1"],
    },
    "heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
    "text-1": { type: "Text", props: { text: "Thanks for signing up." }, children: [] },
  },
};

const html = await renderToHtml(spec);

Spec Structure (Element Tree)

Same flat element tree as @json-render/react: root key plus elements map. Root must be Html; children of Html should be Head and Body. Use Container (e.g. max-width 600px) inside Body for client-safe layout.

Creating a Catalog and Registry

import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToHtml } from "@json-render/react-email";
import { standardComponentDefinitions } from "@json-render/react-email/catalog";
import { Container, Heading, Text } from "@react-email/components";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Alert: {
      props: z.object({
        message: z.string(),
        variant: z.enum(["info", "success", "warning"]).nullable(),
      }),
      slots: [],
      description: "A highlighted message block",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Alert: ({ props }) => (
      <Container style={{ padding: 16, backgroundColor: "#eff6ff", borderRadius: 8 }}>
        <Text style={{ margin: 0 }}>{props.message}</Text>
      </Container>
    ),
  },
});

const html = await renderToHtml(spec, { registry });

Server-Side Render APIs

FunctionPurpose
renderToHtml(spec, options?)Render spec to HTML email string
renderToPlainText(spec, options?)Render spec to plain-text email string

RenderOptions: registry, includeStandard (default true), state (for $state / $cond).

Visibility and State

Supports visible conditions, $state, $cond, repeat (repeat.statePath), and the same expression syntax as @json-render/react. Use state in RenderOptions when rendering server-side so expressions resolve.

Server-Safe Import

Import schema and catalog without React or @react-email/components:

import { schema, standardComponentDefinitions } from "@json-render/react-email/server";

Key Exports

ExportPurpose
defineRegistryCreate type-safe component registry from catalog
RendererRender spec in browser (e.g. preview); use with JSONUIProvider for state/actions
createRendererStandalone renderer component with state/actions/validation
renderToHtmlServer: spec to HTML string
renderToPlainTextServer: spec to plain-text string
schemaEmail element schema
standardComponentsPre-built component implementations
standardComponentDefinitionsCatalog definitions (Zod props)

Sub-path Exports

PathPurpose
@json-render/react-emailFull package
@json-render/react-email/serverSchema and catalog only (no React)
@json-render/react-email/catalogStandard component definitions and types
@json-render/react-email/renderRender functions only

Standard Components

All components accept a style prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.

Document structure

ComponentDescription
HtmlRoot wrapper (lang, dir). Children: Head, Body.
HeadEmail head section.
BodyBody wrapper; use style for background.

Layout

ComponentDescription
ContainerConstrain width (e.g. max-width 600px).
SectionGroup content; table-based for compatibility.
RowHorizontal row.
ColumnColumn in a Row; set width via style.

Content

ComponentDescription
HeadingHeading text (as: h1–h6).
TextBody text.
LinkHyperlink (text, href).
ButtonCTA link styled as button (text, href).
ImageImage from URL (src, alt, width, height).
HrHorizontal rule.

Utility

ComponentDescription
PreviewInbox preview text (inside Html).
MarkdownMarkdown content as email-safe HTML.

Email Best Practices

  • Keep width constrained (e.g. Container max-width 600px).
  • Use inline styles or React Email's style props; many clients strip &lt;style&gt; blocks.
  • Prefer table-based layout (Section, Row, Column) for broad client support.
  • Use absolute URLs for images; many clients block relative or cid: references in some contexts.
  • Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.

Upstream Image

<!-- SYNCED from vercel-labs/json-render (skills/image/SKILL.md) --> <!-- Hash: fc6469e1592a86d4d92058b81704023394ab8cbdd9f422d541ad3a752f2a0e42 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/image

Image renderer that converts JSON specs into SVG and PNG images using Satori.

Quick Start

import { renderToPng } from "@json-render/image/render";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "frame",
  elements: {
    frame: {
      type: "Frame",
      props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
      children: ["heading"],
    },
    heading: {
      type: "Heading",
      props: { text: "Hello World", level: "h1", color: "#ffffff" },
      children: [],
    },
  },
};

const png = await renderToPng(spec, {
  fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" }],
});

Using Standard Components

import { defineCatalog } from "@json-render/core";
import { schema, standardComponentDefinitions } from "@json-render/image";

export const imageCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

Adding Custom Components

import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
});

Standard Components

ComponentCategoryDescription
FrameRootRoot container. Defines width, height, background. Must be root.
BoxLayoutContainer with padding, margin, border, absolute positioning
RowLayoutHorizontal flex layout
ColumnLayoutVertical flex layout
HeadingContenth1-h4 heading text
TextContentBody text with full styling
ImageContentImage from URL
DividerDecorativeHorizontal line separator
SpacerDecorativeEmpty vertical space

Key Exports

ExportPurpose
renderToSvgRender spec to SVG string
renderToPngRender spec to PNG buffer (requires @resvg/resvg-js)
schemaImage element schema
standardComponentsPre-built component registry
standardComponentDefinitionsCatalog definitions for AI prompts

Sub-path Exports

ExportDescription
@json-render/imageFull package: schema, components, render functions
@json-render/image/serverSchema and catalog definitions only (no React/Satori)
@json-render/image/catalogStandard component definitions and types
@json-render/image/renderRender functions only

Upstream Pdf

<!-- SYNCED from vercel-labs/json-render (skills/react-pdf/SKILL.md) --> <!-- Hash: 42d7d378733e0c1e6ff10ba788dc77c32b36b2f28841f1d6aa671e44ed9ad0be --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-pdf

React PDF renderer that generates PDF documents from JSON specs using @react-pdf/renderer.

Installation

npm install @json-render/core @json-render/react-pdf

Quick Start

import { renderToBuffer } from "@json-render/react-pdf";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "doc",
  elements: {
    doc: { type: "Document", props: { title: "Invoice" }, children: ["page"] },
    page: {
      type: "Page",
      props: { size: "A4" },
      children: ["heading", "table"],
    },
    heading: {
      type: "Heading",
      props: { text: "Invoice #1234", level: "h1" },
      children: [],
    },
    table: {
      type: "Table",
      props: {
        columns: [
          { header: "Item", width: "60%" },
          { header: "Price", width: "40%", align: "right" },
        ],
        rows: [
          ["Widget A", "$10.00"],
          ["Widget B", "$25.00"],
        ],
      },
      children: [],
    },
  },
};

const buffer = await renderToBuffer(spec);

Render APIs

import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";

// In-memory buffer
const buffer = await renderToBuffer(spec);

// Readable stream (pipe to HTTP response)
const stream = await renderToStream(spec);
stream.pipe(res);

// Direct to file
await renderToFile(spec, "./output.pdf");

All render functions accept an optional second argument: \{ registry?, state?, handlers? \}.

Standard Components

ComponentDescription
DocumentTop-level PDF wrapper (must be root)
PagePage with size (A4, LETTER), orientation, margins
ViewGeneric container (padding, margin, background, border)
Row, ColumnFlex layout with gap, align, justify
Headingh1-h4 heading text
TextBody text (fontSize, color, weight, alignment)
ImageImage from URL or base64
LinkHyperlink with text and href
TableData table with typed columns and rows
ListOrdered or unordered list
DividerHorizontal line separator
SpacerEmpty vertical space
PageNumberCurrent page number and total pages

Custom Catalog

import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToBuffer } from "@json-render/react-pdf";
import { standardComponentDefinitions } from "@json-render/react-pdf/catalog";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Badge: ({ props }) => (
      <View style={{ backgroundColor: props.color ?? "#e5e7eb", padding: 4 }}>
        <Text>{props.label}</Text>
      </View>
    ),
  },
});

const buffer = await renderToBuffer(spec, { registry });

External Store (Controlled Mode)

Pass a StateStore for full control over state:

import { createStateStore } from "@json-render/react-pdf";

const store = createStateStore({ invoice: { total: 100 } });
store.set("/invoice/total", 200);

Server-Safe Import

Import schema and catalog without pulling in React:

import { schema, standardComponentDefinitions } from "@json-render/react-pdf/server";

Upstream Remotion

<!-- SYNCED from vercel-labs/json-render (skills/remotion/SKILL.md) --> <!-- Hash: 59d63ac06729795f0981fe0cfadf6c1c34454be0329a34f2d36c9be2391fbbe9 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/remotion

Remotion renderer that converts JSON timeline specs into video compositions.

Quick Start

import { Player } from "@remotion/player";
import { Renderer, type TimelineSpec } from "@json-render/remotion";

function VideoPlayer({ spec }: { spec: TimelineSpec }) {
  return (
    <Player
      component={Renderer}
      inputProps={{ spec }}
      durationInFrames={spec.composition.durationInFrames}
      fps={spec.composition.fps}
      compositionWidth={spec.composition.width}
      compositionHeight={spec.composition.height}
      controls
    />
  );
}

Using Standard Components

import { defineCatalog } from "@json-render/core";
import {
  schema,
  standardComponentDefinitions,
  standardTransitionDefinitions,
  standardEffectDefinitions,
} from "@json-render/remotion";

export const videoCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
  transitions: standardTransitionDefinitions,
  effects: standardEffectDefinitions,
});

Adding Custom Components

import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    MyCustomClip: {
      props: z.object({ text: z.string() }),
      type: "scene",
      defaultDuration: 90,
      description: "My custom video clip",
    },
  },
});

// Pass custom component to Renderer
<Player
  component={Renderer}
  inputProps={{
    spec,
    components: { MyCustomClip: MyCustomComponent },
  }}
/>

Timeline Spec Structure

{
  "composition": { "id": "video", "fps": 30, "width": 1920, "height": 1080, "durationInFrames": 300 },
  "tracks": [{ "id": "main", "name": "Main", "type": "video", "enabled": true }],
  "clips": [
    { "id": "clip-1", "trackId": "main", "component": "TitleCard", "props": { "title": "Hello" }, "from": 0, "durationInFrames": 90 }
  ],
  "audio": { "tracks": [] }
}

Standard Components

ComponentTypeDescription
TitleCardsceneFull-screen title with subtitle
TypingTextsceneTerminal-style typing animation
ImageSlideimageFull-screen image display
SplitScreensceneTwo-panel comparison
QuoteCardsceneQuote with attribution
StatCardsceneAnimated statistic display
TextOverlayoverlayText overlay
LowerThirdoverlayName/title overlay

Key Exports

ExportPurpose
RendererRender spec to Remotion composition
schemaTimeline schema
standardComponentsPre-built component registry
standardComponentDefinitionsCatalog definitions
useTransitionTransition animation hook
ClipWrapperWrap clips with transitions
Edit on GitHub

Last updated on

On this page

Multi-Surface Rendering with json-renderQuick ReferenceHow Multi-Surface Rendering WorksQuick Start — Same Catalog, Different RenderersShared Catalog (used by all surfaces)Render to Web (React)Render to PDFRender to EmailRender to OG Image (Satori)Render to Video (Remotion)Render to Terminal (Ink, 0.15+)Render to Next.js App (0.16+)Decision Matrix — When to Use Each TargetPDF Renderer — Reports and DocumentsImage Renderer — OG Images and Social CardsRegistry Mapping — Same Catalog, Platform-Specific ComponentsRule DetailsTarget SelectionReact RendererPDF & Email RendererVideo & Image RendererRegistry MappingKey DecisionsCommon MistakesRelated SkillsRules (5)Use native react-pdf and react-email renderers instead of browser-based workarounds — HIGHPDF & Email RendererPDF Registry PatternUse the Renderer component with catalog validation for web rendering — MEDIUMReact RendererProgressive Streaming PatternCreate separate registries per platform sharing a single catalog — HIGHRegistry MappingType-Safe Registry PatternFile OrganizationSelect renderer target based on output format and platform constraints — HIGHTarget SelectionSelection ChecklistUse Remotion compositions and Satori for video and image generation from specs — MEDIUMVideo & Image RendererSatori CSS ConstraintsCommon OG Image DimensionsReferences (6)Renderer ApiRenderer API Reference@json-render/react&lt;Renderer&gt; ComponentuseStreamingSpec Hook@json-render/react-pdfrenderToBuffer(spec, options): Promise&lt;Buffer&gt;renderToFile(spec, path, options): Promise&lt;void&gt;renderToStream(spec, options): Promise&lt;ReadableStream&gt;@json-render/react-emailrenderToHtml(spec, options): Promise&lt;string&gt;renderToPlainText(spec, options): Promise&lt;string&gt;@json-render/imagerenderToSvg(spec, options): Promise&lt;string&gt;renderToPng(spec, options): Promise&lt;Buffer&gt;@json-render/remotion&lt;JsonRenderComposition&gt; ComponentRemotion Registry with AnimationsCommon Options (all renderers)Target ComparisonTarget Comparison MatrixAll Targets — Capabilities OverviewRegistry Component Primitives by TargetLimitations by TargetPDF (@json-render/react-pdf)Email (@json-render/react-email)Image (@json-render/image / Satori)Remotion (@json-render/remotion)React Native (@json-render/react-native)Performance CharacteristicsWhen to Use Multiple TargetsUpstream Email@json-render/react-emailQuick StartSpec Structure (Element Tree)Creating a Catalog and RegistryServer-Side Render APIsVisibility and StateServer-Safe ImportKey ExportsSub-path ExportsStandard ComponentsDocument structureLayoutContentUtilityEmail Best PracticesUpstream Image@json-render/imageQuick StartUsing Standard ComponentsAdding Custom ComponentsStandard ComponentsKey ExportsSub-path ExportsUpstream Pdf@json-render/react-pdfInstallationQuick StartRender APIsStandard ComponentsCustom CatalogExternal Store (Controlled Mode)Server-Safe ImportUpstream Remotion@json-render/remotionQuick StartUsing Standard ComponentsAdding Custom ComponentsTimeline Spec StructureStandard ComponentsKey Exports