---
name: threads-carousel
user_invocable: true
description: >
  Convert text posts into visual carousel images or presentations for Threads, Instagram, LinkedIn, TikTok, YouTube.
  12 slide types (incl. image/emoji/number), 6 format presets (incl. 1920×1080 wide), 8 background styles (incl. ruled paper), 3-axis style system (font × color × purpose), highlighted keywords with optional italic-box style.
  Generates PNG or single-file PDF via Next.js preview + browser export. RU/EN toolbar.
  Triggers: threads carousel, instagram carousel, linkedin carousel, tiktok carousel, карусель, slides, carousel images, presentation deck, presentation pdf.
---

# 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 within `text`/`title` that 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:

1. User drops a file path like `/Users/me/Desktop/screenshot.png` (or passes a file via the chat).
2. Copy it into `template/public/images/` with a safe, lowercase filename — e.g. `cp "$USER_PATH" template/public/images/screenshot.png`.
3. In `slides.ts` reference it as `imageSrc: "/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 tech
- `paper + orange` — literary warm
- `ember + lime` — dramatic announcement
- `white + coral` — sharp editorial
- `light + 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:

1. **Number of slides** (3–10, default 6)
2. **Format** (see table above, default `threads-4x5`; use `wide-16x9` for a presentation deck)
3. **Purpose** (`carousel` or `presentation`, default `carousel`)
4. **Font** (`minimal` / `editorial` / `clean`, default `minimal`)
5. **Color** (8 palettes, default `dark`)
6. **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

1. **Hook** = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.
2. **Body slides** = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".
3. **Mix slide types** for visual variety: prefer `list` for enumerations, `stats` for numbers, `quote` for direct speech, `comparison` for VS/before-after. Don't make every slide a `body`.
4. **CTA** = conclusion + follow handle. Centered, short.

### Step 4 — Generate

#### Prepare working copy

```bash
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:

```ts
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

```bash
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):** `#0A0A0A` bg, `#FFFFFF` text, `#FACC15` highlight
- **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-image` with Satori + Resvg for CLI export (`bun run export → out/*.png`). Enables headless runs. See `Slashgear/linkedin-carousel-gen` for reference.
- **Per-slide background override** — currently `DEFAULT_BG` is global; could accept a per-slide `bg` field 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.
