Compositions
Compositions are TSX files that describe your image layout. They use a custom JSX runtime — not React — so there are no hooks, no state, and no effects. Just pure functions that return markup.
Overview
Every composition exports two things:
import type { CompositionConfig } from 'grafex';
// 1. Config — sets output dimensions
export const config: CompositionConfig = {
width: 1200,
height: 630,
};
// 2. Default export — the image layout
export default function MyImage() {
return <div style={{ color: 'white' }}>Hello</div>;
} The config is optional. If omitted, Grafex defaults to 1200×630.
The JSX Runtime
Grafex provides its own h() and Fragment functions. You do not need to import React or configure a JSX pragma — Grafex handles the transform automatically via esbuild.
Your components are called once to produce HTML. There is no virtual DOM, no reconciliation, no component lifecycle. Think of compositions as template functions, not interactive components.
What works
- HTML elements (
div,span,img,svg) - Inline styles via
styleprop - Children, nesting, and composition
- Conditional rendering
- Array mapping
- Fragment syntax (
<>...</>)
Does not apply
useState,useEffect, or any hooks- Event handlers (
onClick, etc.) - Client-side interactivity
CompositionConfig
interface CompositionConfig {
width?: number; // Image width in pixels (default: 1200)
height?: number; // Image height in pixels (default: 630)
format?: 'png'; // Output format (only PNG supported)
fonts?: string[]; // URLs to load (e.g., Google Fonts)
} Dimensions can also be overridden at export time via CLI flags (--width, --height) or API options. CLI/API values take precedence over the config export.
Custom Fonts
Load external fonts by providing URLs in config.fonts. Grafex fetches each URL before rendering, so Google Fonts and any other CSS font URL work out of the box.
import type { CompositionConfig } from 'grafex';
export const config: CompositionConfig = {
width: 1200,
height: 630,
fonts: [
'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap',
],
};
export default function Card() {
return (
<h1 style={{ fontFamily: "'Playfair Display', serif" }}>Hello</h1>
);
} Tip:
Any CSS font URL works — Google Fonts, Adobe Fonts, or a self-hosted stylesheet. Grafex injects each URL as a <link> element before rendering.
Styling
Style compositions using the style prop with a CSS-in-JS object:
<div
style={{
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #667eea, #764ba2)',
padding: '40px',
gap: '16px',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0,0,0,0.3)',
}}
>
{/* content */}
</div> Tip:
Unlike Satori, Grafex supports the full CSS spec because it renders in a real browser engine. Flexbox, Grid, calc(), CSS variables, z-index, gradients, shadows, transforms — it all works.
Props
Compositions can accept props for dynamic content. Pass them via the CLI (--props) or the API (options.props).
interface Props {
title: string;
date: string;
}
export default function BlogCard({ title, date }: Props) {
return (
<div style={{ padding: '80px', ... }}>
<div style={{ fontSize: '48px' }}>{title}</div>
<div style={{ color: '#94a3b8' }}>{date}</div>
</div>
);
} Export with props:
grafex export -f blog-card.tsx -o card.png --props '{"title":"My Post","date":"March 2026"}' Example: OG Card
Here's a complete OG image composition and its rendered output. This is the kind of thing you'd generate at build time for every blog post.
export const config = {
width: 600,
height: 400,
};
export default function OgCard() {
return (
<div style={{
width: '100%',
height: '100%',
background: 'linear-gradient(145deg, #0f172a 0%, #1e293b 100%)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '52px',
fontFamily: 'system-ui, -apple-system, sans-serif',
position: 'relative',
overflow: 'hidden',
}}>
{/* Top accent glow */}
<div style={{
position: 'absolute',
top: '-60px',
right: '-60px',
width: '300px',
height: '300px',
background: 'radial-gradient(circle, rgba(244,114,182,0.2) 0%, transparent 70%)',
}} />
{/* Top: tag */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', zIndex: 1 }}>
<div style={{
background: 'rgba(163,230,53,0.15)',
border: '1px solid rgba(163,230,53,0.3)',
borderRadius: '6px',
padding: '4px 12px',
color: '#A3E635',
fontSize: '12px',
fontWeight: '600',
letterSpacing: '0.08em',
textTransform: 'uppercase',
}}>
Tutorial
</div>
<span style={{ color: '#475569', fontSize: '13px' }}>5 min read</span>
</div>
{/* Middle: title */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', zIndex: 1 }}>
<div style={{
color: '#F1F5F9',
fontSize: '36px',
fontWeight: '700',
lineHeight: '1.2',
letterSpacing: '-0.5px',
}}>
Building Modern APIs
</div>
<div style={{ color: '#94A3B8', fontSize: '16px', lineHeight: '1.5' }}>
Best practices for REST and GraphQL in 2026
</div>
</div>
{/* Bottom: author + meta */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '36px',
height: '36px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #F472B6, #38BDF8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '14px',
fontWeight: '700',
flexShrink: 0,
}}>
JS
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
<span style={{ color: '#E2E8F0', fontSize: '14px', fontWeight: '600' }}>Jane Smith</span>
<span style={{ color: '#64748B', fontSize: '12px' }}>March 15, 2026</span>
</div>
</div>
<div style={{
color: '#475569',
fontSize: '13px',
fontWeight: '600',
letterSpacing: '0.05em',
}}>
dev.blog
</div>
</div>
{/* Bottom-left accent stripe */}
<div style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '3px',
background: 'linear-gradient(90deg, #F472B6, #38BDF8, #A3E635)',
}} />
</div>
);
}
Custom Components
Compositions can import and use components from other files. Grafex bundles everything with esbuild before rendering.
interface BadgeProps {
label: string;
color: string;
}
export function Badge({ label, color }: BadgeProps) {
return (
<span style={{ background: color, padding: '4px 12px', ... }}>
{label}
</span>
);
} import type { CompositionConfig } from 'grafex';
import { Badge } from './components/badge';
export const config: CompositionConfig = { width: 1200, height: 630 };
export default function OgImage() {
return (
<div style={{ background: '#1e293b', ... }}>
<Badge label="New" color="#a3e635" />
<div style={{ fontSize: '48px' }}>Grafex v0.1</div>
</div>
);
}