threads-carousel
Threads Carousel Generator
Converts a text post into a set of visual carousel slides for Threads, Instagram, LinkedIn, TikTok, Stories, YouTube, or a standalone presentation deck. Composable design system with three independent style axes (font × color × purpose) and 12 slide types.
Invocation
/threads-carousel <post text>
/threads-carousel path/to/post.md
Format presets (choose target platform)
| Preset | Size | Platforms |
|---|---|---|
threads-4x5 (default) |
1080×1350 | Threads, Instagram feed (portrait) |
instagram-square |
1080×1080 | Instagram, Facebook, LinkedIn feed |
linkedin-square |
1080×1080 | LinkedIn document post (PDF) |
tiktok-9x16 |
1080×1920 | TikTok Photo Mode, Reels, Shorts |
story-9x16 |
1080×1920 | Instagram Stories, Threads Stories |
wide-16x9 |
1920×1080 | Presentations, YouTube, desktop decks |
Slide types (12)
| Type | Purpose | Required fields |
|---|---|---|
hook |
Opening slide — the catchiest line | text |
body |
Title + paragraph | title, text |
body (points) |
Pros/cons list with ✓/✗ SVG icons | title, points[] (instead of text) |
list |
Numbered items (ordered list) | title, items[] |
stats |
Big numbers with labels | title, stats[] |
quote |
Large pulled quote | text, author |
checklist |
Checkmark bullets | title, items[] |
process |
Numbered steps with connector line | title, steps[] |
comparison |
Two-column VS / before-after | leftLabel, leftItems[], rightLabel, rightItems[] |
cta |
Final call to action | text, handle |
image |
Title + screenshot/photo + optional caption | imageSrc, optional title, imageCaption |
emoji |
Giant emoji illustration + title + text | emoji, optional title, text |
number |
Huge hero number/string + title + text | bigNumber, optional title, text |
points shape: Array<{ type: "plus" | "minus"; text: string }> — green ✓ for plus, muted ✗ for minus. Adaptive sizing (44–62px) based on item count and longest line.
All types also support optional:
badge— small outlined tag above title (e.g."01","TIP")highlight— a word or phrase withintext/titlethat will be colored in the preset's highlight color (yellow for dark themes)highlightStyle: "italic-box"— renders the highlighted word in Playfair italic on a colored rectangle (instead of plain color)
Images in slides
image slides expect imageSrc as a path served by Next.js under /public/. Workflow when a user gives you a local file:
- User drops a file path like
/Users/me/Desktop/screenshot.png(or passes a file via the chat). - Copy it into
template/public/images/with a safe, lowercase filename — e.g.cp "$USER_PATH" template/public/images/screenshot.png. - In
slides.tsreference it asimageSrc: "/images/screenshot.png"(absolute from/public/).
Same-origin serving avoids CORS errors in the PNG export pipeline. Do not use external URLs — html-to-image will often blank them out.
Background decorations (8 types)
Switchable via toolbar in preview. Default: glow.
| Type | What it is |
|---|---|
none |
Solid background |
blobs |
Organic colored shapes |
grid |
Dotted grid pattern |
lines |
Diagonal line pattern |
paper |
Ruled notebook lines + left margin (use with paper/light/white colors for a literary feel) |
noise |
SVG grain overlay (overlay blend) |
bignumber |
Giant slide index as watermark (01, 02…) |
glow (default) |
Soft radial gradient in alternating corners |
Style system (3 independent axes)
The final style is composed at runtime from three axes via composePreset(font, color, purpose):
Font axis (DEFAULT_FONT), 5 typefaces:
| Id | Font | Feel |
|---|---|---|
minimal (default) |
Unbounded (body + hook) | Geometric display, bold, distinctive |
editorial |
Playfair Display | Classic serif, literary |
clean |
Inter | Neutral sans-serif, most standard |
mono |
JetBrains Mono | Monospace, tech/dev feel |
condensed |
Oswald | Narrow + tall, editorial poster |
Surface axis (DEFAULT_SURFACE) — bg + text neutrals, 8 options:
| Id | Bg | Text | Feel |
|---|---|---|---|
dark (default) |
#0A0A0A |
white | default bold |
white |
pure #FFFFFF |
near-black | sharp clinical, Apple-style |
light |
cool zinc #F4F4F5 |
near-black | neutral cool grey, Vercel-style |
paper |
cream #ECE2C8 |
warm brown | notebook / literary warm |
gradient |
purple→pink→amber | white | bright gradient |
pastel |
lilac #EDE9FE |
indigo | soft |
neon |
dark gradient | light cyan | tech dark |
ember |
black→red radial | near-white | dramatic |
Accent axis (DEFAULT_ACCENT) — pop color for highlighted words, 8 options:
| Id | Hex | Matches surfaces |
|---|---|---|
yellow (default) |
#FACC15 |
dark, ember |
red |
#DC2626 |
pure red — white, light, paper |
teal |
#14B8A6 |
versatile — dark, white, light, paper |
coral |
#FB7185 |
pink-family — paper, light, white |
orange |
#F97316 |
pumpkin — paper, light, dark |
violet |
#A78BFA |
dark, neon, ember |
lime |
#D9F056 |
ember, dark |
blue |
#3B82F6 |
white, light, paper, dark |
fuchsia |
#C026D3 |
pastel, white, dark |
pink |
#EC4899 |
pastel, dark, light |
amber |
#F59E0B |
gradient, dark, paper |
Total combinations: 8 surfaces × 8 accents × 5 fonts × 2 purposes = 640 valid styles. Pick surface for neutral base, accent for pop color — independently.
Reference combos (inspired by design refs):
dark + teal— noir / minimalist techpaper + orange— literary warmember + lime— dramatic announcementwhite + coral— sharp editoriallight + teal— calm informational
Purpose axis (DEFAULT_PURPOSE):
| Id | Title | Body | Divider |
|---|---|---|---|
carousel (default) |
44px, weight 800, UPPERCASE | weight 600, textColor, line-height 1.2 |
visible (96×4px accent) |
presentation |
72px, weight 700, sentence case | weight 400, textSecondary, line-height 1.45 |
hidden |
Pick purpose: "presentation" + format: "wide-16x9" for a desktop / YouTube presentation deck. Any 3 × 8 × 2 = 48 combinations are valid.
Workflow
Step 1 — Get the text
- If passed inline — use it.
- If a file path (
.md,.txt) — read it. - If text is long (>500 chars) — confirm it's complete before planning.
Step 2 — Clarify parameters (optional, can assume defaults)
Ask once, combined:
- Number of slides (3–10, default 6)
- Format (see table above, default
threads-4x5; usewide-16x9for a presentation deck) - Purpose (
carouselorpresentation, defaultcarousel) - Font (
minimal/editorial/clean, defaultminimal) - Color (8 palettes, default
dark) - Handle for CTA slide (e.g.
@username)
Shortcut: if user says "presentation" / "презентация" / "slide deck" → default to purpose: presentation, format: wide-16x9, font: clean, color: white.
If user says "your call" — apply defaults, do not block.
Step 3 — Plan the slide breakdown
Show the user a preview list before generation:
Slide 1 (hook): "Headline..." [highlight: "word"]
Slide 2 (body): badge 01 — "Title" / "Text..."
Slide 3 (list): badge 02 — "iOS stack" / 3 items
Slide 4 (stats): badge 03 — "Growth" / 3 stats
Slide 5 (quote): "Quote..." — Author
Slide 6 (cta): "Final message" @username
Rules for splitting text into slides
- Hook = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.
- Body slides = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".
- Mix slide types for visual variety: prefer
listfor enumerations,statsfor numbers,quotefor direct speech,comparisonfor VS/before-after. Don't make every slide abody. - CTA = conclusion + follow handle. Centered, short.
Step 4 — Generate
Prepare working copy
WORK_DIR="/tmp/carousel-$(date +%s)"
rsync -a --exclude=node_modules ~/.claude/skills/threads-carousel/template/ "$WORK_DIR/"
ln -s ~/.claude/skills/threads-carousel/template/node_modules "$WORK_DIR/node_modules"
cd "$WORK_DIR"
Symlinking node_modules avoids the 350MB copy per run.
Inject content into src/slides.ts
All content + defaults live in src/slides.ts — never touch the engine (src/app/CarouselApp.tsx, src/lib/*). Edit the SLIDES array and the 5 default constants.
Full injection example:
import type { SlideData, BgType, FormatId, FontId, ColorThemeId, PurposeId } from "./lib/types";
export const SLIDES: SlideData[] = [
{ type: "hook", text: "Line one\nline two", highlight: "two" },
{ type: "body", badge: "01", title: "Title", text: "Body text...", highlight: "key" },
{ type: "body", badge: "02", title: "Pros & Cons", points: [
{ type: "plus", text: "One click to register" },
{ type: "plus", text: "Works on any background" },
{ type: "minus", text: "Requires provider setup" },
]},
{ type: "list", badge: "03", title: "Steps", items: ["First", "Second", "Third"] },
{ type: "stats", title: "Numbers", stats: [
{ value: "10×", label: "Faster" },
{ value: "50%", label: "Smaller" },
]},
{ type: "quote", text: "Big idea\nin few words", author: "Someone", role: "2026" },
{ type: "checklist", title: "Pre-flight", items: ["One", "Two", "Three"] },
{ type: "cta", text: "Last word", handle: "@username" },
];
export const DEFAULT_FONT: FontId = "minimal";
export const DEFAULT_COLOR: ColorThemeId = "dark";
export const DEFAULT_PURPOSE: PurposeId = "carousel";
export const DEFAULT_BG: BgType = "glow";
export const DEFAULT_FORMAT: FormatId = "threads-4x5";
Launch preview and export
bun dev --port 3333
Tell the user to open http://localhost:3333. They can:
- Switch Format / Mode / Font / Color / Background live via toolbar rows
- Toggle UI language RU / EN in the top-right
- Click PDF to download all slides in one file (JPEG-compressed, ~5–8 MB for 10 slides)
- Click PNG (a.k.a. "Export All") to download every slide as
01-hook.png,02-body.png, … - Click an individual slide thumbnail to export just that one as PNG
After export, stop the dev server.
Parallel carousels
If making multiple carousels at once: create multiple work dirs and launch on ports 3333, 3334, 3335 — they run side by side without conflict.
Design system
Canonical look (font: minimal, color: dark, purpose: carousel):
- Display typeface: Unbounded (Google Fonts), for hooks — applied only when the font axis provides
hookFontFamily - Body typeface: Space Grotesk (minimal) / Playfair (editorial) / Inter (clean) — everything else uses
preset.fontFamily - Palette (dark):
#0A0A0Abg,#FFFFFFtext,#FACC15highlight - Layout: 80px padding, left-aligned, slide counter bottom-center
- Title discipline: carousel purpose — title → a 96×4px accent divider → body, with ≥64px breathing room above body. Presentation purpose — no divider, sentence case, 72px.
- Hook size: 104–140px, adaptive by character/line count
- Body text size: 48–88px, adaptive
- Text balance:
textWrap: "balance"on hook + title (no orphan words)
Typography table (carousel purpose)
The presentation purpose overrides titles to 72px / 700 / sentence case and body to 400 / textSecondary / line-height 1.45.
| Element | Size | Weight | Font source |
|---|---|---|---|
| Hook | 104–140px | 800 | hookFontFamily ?? fontFamily |
| Title | 44px | 800 uppercase | fontFamily |
| Body | 48–88px | 600 | fontFamily |
| Points (pros/cons) | 44–62px | 600 | fontFamily |
| Badge | 26px | 800 uppercase | fontFamily |
| Stats value | 140–170px | 900 | fontFamily |
| Stats label | 32px | 500 uppercase | fontFamily |
| Quote | 62px | 600 | fontFamily |
| List item | 46px | 600 | fontFamily (numbers 48px) |
| Checklist item | 44px | 600 | fontFamily |
| Process step title | 36px | 700 | fontFamily |
| Handle | 36px | 500 | fontFamily |
Common mistakes
| Mistake | Fix |
|---|---|
| Too much text on a slide | Max 40 words, max 5 lines |
| Two ideas on one slide | Split into two slides |
All slides are body type |
Mix in list, stats, quote, checklist for visual variety |
| No hook on first slide | Slide 1 must be the catchiest line |
| No CTA on last slide | Slide N must end with a handle or call to action |
| Highlight word too long | Keep highlight to 1–2 words, not whole phrase |
| Badge has too many characters | Max 2–4 characters (01, TIP, NEW) |
Future work / TODOs
- Satori server-side export — replace browser-based
html-to-imagewith Satori + Resvg for CLI export (bun run export → out/*.png). Enables headless runs. SeeSlashgear/linkedin-carousel-genfor reference. - Per-slide background override — currently
DEFAULT_BGis global; could accept a per-slidebgfield to mix decorations across a deck. - Cyrillic-optimized adaptive sizing — current thresholds are calibrated for Latin; Russian copy tends to be 20–30% longer at the same font size.
- Pencil MCP mode — previous skill version had a manual design mode via Pencil; not currently implemented in the template.