Skip to content
Grafex
GitHub

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:

example.tsx
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 style prop
  • 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

TypeScript
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.

card.tsx
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:

example.tsx
<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).

blog-card.tsx
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.

og-card.tsx
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>
  );
}
og-card.png 600 × 400
Rendered OG card: Building Modern APIs

Custom Components

Compositions can import and use components from other files. Grafex bundles everything with esbuild before rendering.

components/badge.tsx
interface BadgeProps {
  label: string;
  color: string;
}

export function Badge({ label, color }: BadgeProps) {
  return (
    <span style={{ background: color, padding: '4px 12px', ... }}>
      {label}
    </span>
  );
}
og-image.tsx
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>
  );
}