- name
- poster
- description
- Render a standalone React component (Tailwind + Recharts + lucide-react) to PNG / SVG / PDF / JPG / WebP via the `poster_render` tool. Use when the user asks for a chart, dashboard, report card, social-share image, OG image, year-in-review, editorial data story, magazine layout, or any visual deliverable that should come out as a single image/document file. Not for interactive UIs; posters are compositions.
# poster
Render a single-file React component to an image. One tool: **`poster_render`**.
Write TSX like a graphic designer composes a poster, not like a web developer building a page. Everything is fixed-width, non-responsive, pixel-precise, and optimized to look **amazing at thumbnail size on a social feed**.
## When to reach for `poster_render`
Chart, dashboard snapshot, OG image, social-share card, year-in-review, editorial data story, magazine layout, event poster, calendar, cover image, PDF one-pager, README hero. Anything that's a *composition* meant to be looked at, not interacted with.
**Not** for: interactive UIs, forms, multi-page sites, anything needing runtime JS behavior after render.
---
## The contract
These aren't style preferences — break them and the tool rejects the render.
### 1. The root element declares the canvas — this is the only source of truth
**The tool has no `width` or `height` parameter.** The root <div>'s Tailwind size IS the canvas. The renderer measures it exactly.
```tsx
// ✓ width only — height emerges from content (use this 80% of the time)
<div className="w-[1600px] p-10 ...">
// ✓ width + height — fixed aspect for magazine covers, story format, etc.
<div className="w-[1080px] h-[1350px] p-10 ...">
// ✗ rejected — no root width (canvas defaults, usually wrong)
<div className="p-10 ...">
// ✗ rejected — min-h-screen stretches to viewport, not canvas
<div className="w-[1600px] min-h-screen ...">
// ✗ rejected — w-full next to w-[Npx] overrides the explicit width
<div className="w-[1600px] w-full ...">
// ✗ brittle — aspect without explicit width is indeterminate
<div className="aspect-[4/5] p-10 ...">
```
If you use absolute-positioned decorations (`top: 40%`, gradient blobs), you need a definite parent height — add `h-[Npx]` or `min-h-[Npx]` to the root.
### 2. Font-size floor: 14px
Anything ≤12px disappears when the poster is viewed at half scale on a feed. Don't go below `text-sm` (14px) or `text-[14px]`. Recharts axis ticks: `fontSize: 13`. Chart-internal SVG labels: `fontSize: 13+`.
### 3. Use the three bundled fonts
No imports needed. They're loaded in the shell:
- **`Inter, system-ui`** — default sans
- **`'Source Serif 4', serif`** — magazine / editorial / italic "reveal words"
- **`'JetBrains Mono', ui-monospace, monospace`** — code, mono metadata
Set via inline `style={{ fontFamily: "..." }}` on the root.
### 4. Canvas size by shape
| shape | width | height | use |
|---|---|---|---|
| twitter / landscape | 1600 | auto or 900 | thread images, cover images |
| OG image | 1200 | 630 | social previews (1.91:1) |
| instagram square | 1200 | 1200 | social square |
| story / wrapped | 1080 | 1350 | vertical, mobile-first |
| editorial / magazine | 1400 | 1800 | tall data story |
| dashboard | 1600 | 1000 | metrics grid |
| poster / cover | 1200 | 1600 | print-feel |
| weather / hero card | 1400 | 900 | glass-card compositions |
Pick a shape, write `w-[Npx]` (add `h-[Npx]` only if fixed-aspect), let auto-fit do the rest.
---
## The layout grammar
Every good poster uses these primitives. Mix and match.
### Header row: kicker + title (left) + chip (right)
```tsx
<header className="flex items-end justify-between">
<div>
<div className="text-[14px] font-bold uppercase tracking-[0.3em] text-white/50">
Pi · Monday, 16 April 2026
</div>
<h1 className="mt-2 text-5xl font-black tracking-tight">
Good morning
</h1>
</div>
<div className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[14px] text-white/60">
Live · last 7 days
</div>
</header>
```
Rule of thumb: **kicker = meta context, title = the answer, right-side chip = status/time**.
### Eyebrow kicker (the "small-caps label")
```tsx
<div className="text-[14px] font-bold uppercase tracking-[0.3em] text-white/50">
FIG. 1 — GLOBAL TEMPERATURE ANOMALY
</div>
```
- `text-[14px]` to `text-[15px]`
- `font-semibold` to `font-bold`
- `uppercase` always
- `tracking-[0.2em]` to `tracking-[0.5em]` — more tracking = more formal
- muted color: `text-white/40` to `text-white/60` on dark, `text-neutral-500` on light
### Italic "reveal word" in a headline
The signature move. Break a headline with a gradient-filled Source Serif 4 italic:
```tsx
<h1 className="text-7xl font-black tracking-tight leading-[0.9]">
A century and a half of{" "}
<em
className="italic font-normal"
style={{
fontFamily: "'Source Serif 4', serif",
background: "linear-gradient(180deg,#fef3c7 0%,#f472b6 55%,#a855f7 100%)",
WebkitBackgroundClip: "text",
color: "transparent",
}}
>
warming,
</em>{" "}
charted in one line.
</h1>
```
The pastel-to-fuchsia-to-violet gradient is the most reusable "poster-ai look". Other good ones:
- `#fef3c7 → #f97316 → #dc2626` (cream → orange → crimson)
- `#22d3ee → #8b5cf6` (cyan → violet)
- `#fde68a → #f59e0b → #ec4899` (butter → amber → pink)
### Card (the workhorse container)
```tsx
<div
className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5"
style={{ boxShadow: "inset 0 1px 0 0 rgba(255,255,255,0.04), 0 20px 40px -24px rgba(0,0,0,0.6)" }}
>
{children}
</div>
```
- `rounded-xl` (12px) to `rounded-3xl` (24px)
- subtle border: `border-white/[0.05-0.15]` on dark, `border-neutral-200/80` on light
- subtle fill: `bg-white/[0.02-0.08]` on dark, pure `bg-white` on light
- inset top highlight + offset shadow for floating feel
### KPI stat
```tsx
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5">
<div className="flex items-center justify-between">
<div className="flex h-9 w-9 items-center justify-center rounded-xl"
style={{ background: "linear-gradient(135deg,#22d3ee,#3b82f6)" }}>
<TrendingUpIcon className="h-4 w-4 text-white" />
</div>
<span className="text-[14px] font-medium text-emerald-400">+18.4%</span>
</div>
<div className="mt-4 text-[14px] uppercase tracking-wider text-white/40">ARR</div>
<div className="mt-1 text-2xl font-semibold tabular-nums">$14.8M</div>
</div>
```
Gradient icon square + label + value + delta chip. Use `tabular-nums` on every number.
### Hero metric (the big single number)
```tsx
<div>
<div className="text-[14px] font-bold uppercase tracking-[0.3em] text-white/80">
You listened to
</div>
<div className="mt-2 font-black leading-[0.82] tracking-tighter tabular-nums"
style={{ fontSize: 220 }}>
586
</div>
<div className="mt-1 text-3xl font-bold">hours of music</div>
<div className="mt-1 text-lg text-white/80">
That's longer than 83% of listeners in Portugal.
</div>
</div>
```
Kicker → ENORMOUS number (180-260px) → unit → context sentence. Set `fontSize` inline when going above `text-9xl`.
### Activity ring (Apple-Watch style)
```tsx
const stroke = 18, r = (200 - stroke) / 2, c = 2 * Math.PI * r;
const pct = Math.min(value / goal, 1.4);
<svg width={200} height={200} className="-rotate-90">
<circle cx={100} cy={100} r={r} stroke={color} strokeOpacity={0.15}
strokeWidth={stroke} fill="none" />
<circle cx={100} cy={100} r={r} stroke={color} strokeWidth={stroke}
strokeLinecap="round" fill="none"
strokeDasharray={c} strokeDashoffset={c * (1 - pct)}
style={{ filter: `drop-shadow(0 0 8px ${color}99)` }} />
</svg>
```
### Contribution heatmap (GitHub-style)
```tsx
const levelColor = ["rgba(255,255,255,0.05)", "#0e4429", "#006d32", "#26a641", "#39d353"];
<div className="flex gap-[3px]">
{weeks.map((week, w) => (
<div key={w} className="flex flex-col gap-[3px]">
{week.map((level, d) => (
<div key={d} className="h-[14px] w-[14px] rounded-sm"
style={{ background: levelColor[level] }} />
))}
</div>
))}
</div>
```
### Footer
```tsx
<footer className="mt-6 flex items-center justify-between border-t border-white/5 pt-4 text-[14px] text-white/30">
<span>poster · generated 2026-04-16</span>
<span>nebula.dev/pulse</span>
</footer>
```
---
## Color systems
Pick ONE family and stick to it. Mixing accents = muddy.
### Dark theme bases
| color | use |
|---|---|
| `#05050a` | black with fuchsia cast |
| `#07060d` / `#0b0a12` | black with violet cast |
| `#0a0a0f` | neutral near-black |
| `#0d1117` | GitHub black |
| `#07080c` | blue-black |
### Light theme bases
| color | use |
|---|---|
| `#fafaf7` | editorial off-white |
| `#faf5ed` | warm paper |
| `#f5e8de` | magazine cream |
| `#fef08a` | brutalist yellow |
| `#fef3c7` | pastel cream |
### Accent families (pick one)
| family | swatches | vibe |
|---|---|---|
| **cyan/violet** | `#22d3ee`, `#3b82f6`, `#a78bfa`, `#7c3aed` | tech, analytics, premium |
| **amber/rose** | `#fbbf24`, `#f97316`, `#fb7185`, `#e11d48` | warm, retail, alerts |
| **emerald** | `#10b981`, `#34d399`, `#39d353` | growth, health, positive |
| **fuchsia/violet** | `#ec4899`, `#f472b6`, `#a855f7`, `#6d28d9` | consumer, energy, wrapped |
### Gradient recipes
```ts
// Radial "hotspot" — use two at opposite corners for depth
"radial-gradient(800px 500px at 90% 0%, rgba(139,92,246,0.18), transparent 60%), #0a0a0f"
// Pastel reveal gradient (for gradient text)
"linear-gradient(180deg,#fef3c7 0%,#f472b6 55%,#a855f7 100%)"
// Tech gradient (for icon squares)
"linear-gradient(135deg,#22d3ee,#3b82f6)"
// Sunset (for story backgrounds)
"radial-gradient(ellipse at top, #7c3aed 0%, #ec4899 40%, #f97316 75%, #fbbf24 100%)"
```
### Grain overlay (adds poster-print feel)
```tsx
<div
className="pointer-events-none absolute inset-0 opacity-[0.08] mix-blend-overlay"
style={{
backgroundImage: "repeating-radial-gradient(circle at 20% 30%, white 0, white 1px, transparent 1px, transparent 4px)",
}}
/>
```
---
## Recharts idioms
Posters use Recharts differently than an app does. No tooltips (static render), no legend chrome, minimal axes, gradient fills.
### Always
- Wrap in `<ResponsiveContainer width="100%" height="100%">` inside a fixed-height parent
View on GitHub