Images as Code. Write JSX, export as images.
Grafex is a programmatic image composition tool. Write compositions in JSX/TSX with full CSS support and export as images — no browser window, no server, no configuration ceremony.
npx grafex export -f card.tsx -o card.png
- Node.js >= 20.0.0
- WebKit browser (installed automatically on
npm install)
npm install grafexScaffold the starter files:
npx grafex initThis creates a tsconfig.json with the correct JSX settings and a composition.tsx starter file.
Write a composition:
// card.tsx
import type { CompositionConfig } from 'grafex';
export const config: CompositionConfig = { width: 1200, height: 630 };
export default function Card() {
return (
<div
style={{
width: '100%',
height: '100%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '64px',
fontWeight: 'bold',
fontFamily: 'system-ui, sans-serif',
}}
>
Hello, Grafex!
</div>
);
}Export it:
npx grafex export -f card.tsx -o card.pngRun grafex init to generate the correct tsconfig.json automatically:
npx grafex initOr add these compiler options to your tsconfig.json manually:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}Grafex ships its own JSX type definitions (JSX.Element, JSX.IntrinsicElements, etc.). They are picked up automatically once the package is installed — no extra imports needed. The h and Fragment functions are injected at transpile time, so you never import them in composition files.
Scaffold the minimal setup to start writing compositions. Run this in a new project after npm install grafex.
npx grafex initCreates two files in the current directory:
tsconfig.json— TypeScript config with the correct JSX settings for Grafexcomposition.tsx— A starter composition with a "Hello, Grafex!" example
If either file already exists, it is skipped without error.
| Flag | Short | Description |
|---|---|---|
--help |
-h |
Show help text |
Render a composition file to an image.
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--file |
-f |
string | — | Path to the .tsx composition file (required) |
--out |
-o |
string | ./output.png |
Output file path or directory (for multi-variant output) |
--props |
string (JSON) | {} |
Props to pass to the composition as a JSON object | |
--width |
number | from config |
Override composition width in pixels | |
--height |
number | from config |
Override composition height in pixels | |
--format |
string | png |
Output format (png or jpeg) |
|
--quality |
number | 90 |
JPEG quality 1–100 (only applies when format is jpeg) |
|
--scale |
number | 1 |
Device pixel ratio. Use 2 for retina/high-DPI output. |
|
--browser |
string | webkit |
Browser engine: webkit or chromium |
|
--variant |
string | (all) | Render a single variant by name. Omit to render all variants. | |
--help |
-h |
Show help text |
High-DPI output: Set
scaleto control the device pixel ratio. A 1200x630 composition withscale: 2produces a 2400x1260 PNG — same layout, double the pixel density. Works in the config, CLI, and API.export const config: CompositionConfig = { width: 1200, height: 630, scale: 2, };
Examples:
# Basic export
grafex export -f card.tsx -o card.png
# Pass props to the composition
grafex export -f card.tsx -o card.png --props '{"title":"Hello World"}'
# Override dimensions
grafex export -f card.tsx -o card.png --width 800 --height 400
Start a live preview server that watches your composition and re-renders on every file change.
npx grafex dev -f card.tsx| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--file |
-f |
string | — | Path to the .tsx composition file (required) |
--port |
number | 3000 |
Preview server port | |
--props |
string (JSON) | {} |
Props to pass to the composition as a JSON object | |
--variant |
string | (first) | Show only the named variant (when composition has variants) | |
--help |
-h |
Show help text |
The dev server watches the composition file, all its imports, CSS files from config.css, and local image assets. Changes are debounced and the preview updates within ~100ms. Open http://localhost:3000 to see the live preview.
Press Ctrl+C to stop.
grafex --version # Print version and exit
grafex --help # Print help text and exitimport { render, renderAll, close } from 'grafex';Render a composition to an image buffer. Pass options.variant to render a specific variant from config.variants.
const result = await render('./card.tsx', {
props: { title: 'Hello' },
width: 1200,
height: 630,
});
// result.buffer — Buffer containing image data
// result.width — effective render width
// result.height — effective render height
// result.scale — device pixel ratio used
// result.format — 'png' | 'jpeg'Parameters:
| Parameter | Type | Description |
|---|---|---|
compositionPath |
string |
Path to the .tsx composition file |
options.props |
Record<string, unknown> |
Props to pass to the composition |
options.width |
number |
Override composition width |
options.height |
number |
Override composition height |
options.format |
'png' | 'jpeg' |
Output format (default: 'png') |
options.quality |
number |
JPEG quality 1–100 (default: 90, only applies to JPEG) |
options.scale |
number |
Device pixel ratio (default: 1) |
options.browser |
'webkit' | 'chromium' |
Browser engine (default: 'webkit') |
options.variant |
string |
Named variant to render from config.variants |
Returns: Promise<RenderResult> where RenderResult is:
interface RenderResult {
buffer: Buffer;
width: number;
height: number;
scale: number;
format: 'png' | 'jpeg';
}Render all variants defined in config.variants. Returns a Map<string, RenderResult> keyed by variant name.
import { renderAll, close } from 'grafex';
import { writeFileSync } from 'node:fs';
const all = await renderAll('./card.tsx', { props: { title: 'Hello' } });
for (const [name, result] of all) {
writeFileSync(`${name}.${result.format}`, result.buffer);
}
await close();Shut down the browser process. Call this when you are done rendering to free resources.
await close();Example — render multiple compositions:
import { render, close } from 'grafex';
import { writeFileSync } from 'node:fs';
const compositions = ['hero.tsx', 'card.tsx', 'thumbnail.tsx'];
for (const file of compositions) {
const result = await render(file);
writeFileSync(file.replace('.tsx', '.png'), result.buffer);
}
await close();import { h, Fragment, renderToHTML, BrowserManager } from 'grafex';Set arbitrary attributes on the root <html> element via config.htmlAttributes. This enables CSS selectors like :root[data-theme="dark"] used by Tailwind v4 and other theming systems:
export const config: CompositionConfig = {
width: 1200,
height: 630,
htmlAttributes: {
'data-theme': 'dark',
lang: 'en',
},
};This renders as <html data-theme="dark" lang="en">. Attribute values are HTML-escaped automatically.
Variants can override htmlAttributes — the variant's attributes are spread over the base config's attributes:
export const config: CompositionConfig = {
width: 1200,
height: 630,
htmlAttributes: { 'data-theme': 'light' },
variants: {
light: {},
dark: { htmlAttributes: { 'data-theme': 'dark' } },
},
};Load external CSS files by specifying paths in config.css. Paths are resolved relative to the composition file:
export const config: CompositionConfig = {
width: 1200,
height: 630,
css: ['./styles.css'],
};This works with any CSS — plain stylesheets, Tailwind output, Sass output, anything that produces a .css file. The contents are injected as <style> tags in the HTML <head> before rendering.
Tailwind CSS example:
# 1. Generate the CSS
npx tailwindcss -i ./input.css -o ./styles.css// card.tsx
export const config: CompositionConfig = {
width: 1200,
height: 630,
css: ['./styles.css'],
};
export default function Card() {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-indigo-500 to-purple-600 text-white">
<h1 className="text-6xl font-bold">Hello Tailwind</h1>
</div>
);
}# 2. Export the composition
npx grafex export -f card.tsx -o card.pngLive dev workflow with Tailwind:
Run Tailwind's --watch mode in one terminal and grafex dev in another. When you edit your composition, Tailwind recompiles styles.css and Grafex detects the CSS change and re-renders automatically — no manual steps in between.
# Terminal 1 — Tailwind watcher
npx @tailwindcss/cli -i ./input.css -o ./styles.css --watch
# Terminal 2 — Grafex dev server
npx grafex dev -f card.tsxOr wire both into a single npm run dev command with concurrently:
{
"scripts": {
"dev": "concurrently \"npx @tailwindcss/cli -i ./input.css -o ./styles.css --watch\" \"npx grafex dev -f card.tsx\"",
"build": "npx @tailwindcss/cli -i ./input.css -o ./styles.css && npx grafex export -f card.tsx -o card.png"
},
"devDependencies": {
"@tailwindcss/cli": "^4.0.0",
"concurrently": "^9.0.0"
}
}Key DX feature:
grafex devwatches all CSS files listed inconfig.css. When Tailwind's--watchrewritesstyles.css, Grafex picks up the change and re-renders within ~100ms. The two tools compose naturally — no plugins, no custom config.If
styles.cssdoesn't exist yet whengrafex devstarts, it renders without styles and re-renders automatically as soon as Tailwind creates the file — no pre-build step needed.
See the examples/tailwind/ directory for a complete working example.
Use local image files in <img> tags or CSS background-image. Grafex reads them from disk and embeds them as base64 data URLs automatically — no server or public URL needed.
export default function Card() {
return (
<div style={{ width: '100%', height: '100%' }}>
<img src="./logo.png" alt="Logo" width="200" height="60" />
</div>
);
}Paths are resolved relative to the composition file. Supported formats: PNG, JPEG, GIF, WebP, SVG, AVIF, ICO, BMP.
CSS url() references work too — both in inline styles and in external CSS files loaded via config.css:
.hero {
background-image: url('./hero.jpg');
}Remote URLs (http://, https://) and data URLs are passed through unchanged.
Produce multiple outputs from a single composition — different sizes, formats, or props. Define a variants record in your config. Each variant inherits from the base config and can override any field:
import type { CompositionConfig } from 'grafex';
export const config: CompositionConfig = {
width: 1200,
height: 630,
variants: {
og: {},
twitter: { height: 675 },
square: { width: 1080, height: 1080, props: { layout: 'square' } },
},
};
export default function Card({ layout = 'default' }: { layout?: string }) {
return <div style={{ width: '100%', height: '100%' }}>{layout}</div>;
}Export all variants:
# Renders og.png, twitter.png, square.png (named after each variant)
grafex export -f card.tsx
# Same, but into a directory
grafex export -f card.tsx -o ./images/Export a single variant:
grafex export -f card.tsx --variant og -o card-og.pngLibrary API:
import { render, renderAll, close } from 'grafex';
// Single variant
const result = await render('./card.tsx', { variant: 'twitter' });
// All variants
const all = await renderAll('./card.tsx');
for (const [name, result] of all) {
writeFileSync(`${name}.${result.format}`, result.buffer);
}
await close();Merge rules:
- CLI/API options override variant config, which overrides base config
propsare shallow-merged: variant props apply first, then CLI/API props override individual keys- Array fields (
fonts,css) replace the base value — they do not merge htmlAttributes: variant attributes are spread over base config attributes (shallow merge)
WebKit is downloaded automatically when you run npm install via the postinstall script:
npm install grafex
# WebKit is installed automaticallyTo install manually:
npx playwright install webkitTo install only the browser binary without system dependencies:
npx playwright-core install webkitChromium (alternative engine): Grafex also supports Chromium if you hit CSS compatibility issues. Install it separately and pass --browser chromium to the CLI or browser: 'chromium' in the API:
npx playwright install chromium
grafex export -f card.tsx -o card.png --browser chromiumBy default, Playwright installs browsers to a shared cache directory. Set PLAYWRIGHT_BROWSERS_PATH to control where the browser binary is stored:
export PLAYWRIGHT_BROWSERS_PATH=/path/to/browsers
npx playwright install webkit
grafex export -f card.tsx -o card.pngThis is useful in CI environments where the home directory may not be writable.
On Linux, WebKit requires system dependencies. Install them with:
npx playwright install-deps webkit
npx playwright install webkitGitHub Actions example:
- name: Install WebKit dependencies
run: npx playwright install-deps webkit
- name: Install WebKit
run: npx playwright install webkit
- name: Export image
run: npx grafex export -f card.tsx -o card.pngWe welcome contributions! See CONTRIBUTING.md for development setup, architecture overview, code standards, and PR guidelines.
MIT