instagram-carousel
Creates high-quality Instagram carousels as swipeable HTML previews with export-ready slides (1080×1350px PNG). Handles the full workflow: brand setup, slide copy, visual design system (colors, fonts, components), HTML generation, and Playwright-based export. Use this skill whenever the user asks to create, design, or generate an Instagram carousel, carrossel, slides para Instagram, or any Instagram multi-image post — even if they don't explicitly say "carousel" or "skill". Also trigger for requests to "create a post with multiple slides", "fazer carrossel", or "exportar slides para o Instagram".
How do I install this agent skill?
npx skills add https://github.com/marcolang/marketing-skills --skill instagram-carouselIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill generates Instagram carousels by creating HTML and using Playwright to capture screenshots. It automates the installation of Playwright and reads local files to embed images. The primary risk is that it processes data from user-provided paths and URLs without explicit sanitization, which could lead to sensitive file access or script execution within the rendering environment if malicious inputs are provided.
- Socketwarn
1 alert: gptAnomaly
- Snykpass
Risk: LOW · No issues
- ZeroLeakspass
2 findings · Score: 80/100
What does this agent skill do?
Instagram Carousel Generator
Generates fully self-contained, swipeable HTML carousels where every slide is designed to be exported as an individual 1080×1350px PNG for Instagram.
Step 1: Collect Brand Details
Before generating, ask the user for the following (if not already provided):
- Brand name — displayed on first and last slides
- Instagram handle — shown in the IG frame header
- Primary brand color — hex code, or describe and Claude picks one
- Logo — SVG path, brand initial, or skip
- Font preference — see typography table below, or specific Google Fonts
- Tone — professional, casual, playful, bold, minimal, etc.
- Images — profile photo, screenshots, product images, etc.
- Idioma dos slides — default: Português (BR) unless specified otherwise
- Carousel format — standard (7 slides) or alternate sequence (see sequences section)
If the user provides a website URL or brand assets, derive colors and style from those.
If the user says "make me a carousel about X" without brand details, ask before generating. Don't assume defaults.
Handling User-Provided Images
This section applies from the very first HTML generation — not only during export.
When the user provides an image file path (e.g., /home/user/gestante.png, /mnt/user-data/uploads/foto.jpg):
⚠️ Critical Rules
- NEVER use relative paths (
gestante.png) — they break in every browser context except the exact folder the HTML lives in. - NEVER use
background: url(filepath)— leads to 1.5MB+ base64 inline strings that crash the browser parser. - ALWAYS embed as base64
data:URI — works in preview, export, and any environment. - ALWAYS generate the HTML via Python (
Path.write_text()) — shell heredocs interpolate$and backticks, corrupting base64 strings.
Step-by-step: embed an image
# 1. Check the actual file format (extension may lie)
file /path/to/image.png
import base64
from pathlib import Path
# 2. Read and encode
img_path = Path("/path/to/image.png")
# Use "image/jpeg" if `file` command says JPEG, else "image/png"
mime = "image/jpeg" # or "image/png"
b64 = base64.b64encode(img_path.read_bytes()).decode()
data_uri = f"data:{mime};base64,{b64}"
# 3. Inject into HTML template as a Python variable — never via shell
html = f"""
<div style="position:relative;width:100%;height:100%;">
<img src="{data_uri}"
style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;">
<div style="position:absolute;inset:0;background:rgba(255,255,255,0.35);z-index:1;"></div>
<!-- slide content goes here, z-index:2 -->
</div>
"""
Path("/home/claude/carousel.html").write_text(html, encoding="utf-8")
Image as slide background (most common use)
<!-- Inside the slide div, before any content -->
<img src="{data_uri}"
style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;">
<!-- Semi-transparent overlay so text stays readable -->
<div style="position:absolute;inset:0;background:rgba(255,255,255,0.35);z-index:1;"></div>
<!-- All slide content must have z-index:2 or higher -->
For dark slides, use rgba(0,0,0,0.45) as the overlay instead.
Common image mistakes to avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
<img src="gestante.png"> | Broken image — relative path only works if HTML and image share the same folder | Always use base64 data: URI |
background: url('data:...') inline with 1.5MB base64 | Browser parser crash, 1.3M token context | Use <img> tag with object-fit:cover |
Generating HTML via shell echo or heredoc | $ and backtick characters in base64 get interpolated and corrupt the string | Always use Python Path.write_text() |
Assuming .png extension = PNG format | File may actually be JPEG; wrong MIME type breaks rendering | Run file command to detect actual format |
Step 2: Derive the Full Color System
From the user's single primary brand color, generate the full 6-token palette:
BRAND_PRIMARY = {user's color} // Main accent — progress bar, icons, tags
BRAND_LIGHT = {primary lightened ~20%} // Secondary accent — tags on dark, pills
BRAND_DARK = {primary darkened ~30%} // CTA text, gradient anchor
LIGHT_BG = {warm or cool off-white} // Light slide background (never pure #fff)
LIGHT_BORDER = {slightly darker than LIGHT_BG} // Dividers on light slides
DARK_BG = {near-black with brand tint} // Dark slide background
Rules for deriving colors:
- LIGHT_BG: tinted off-white complementing the primary (warm → warm cream, cool → cool gray-white)
- DARK_BG: near-black with subtle brand tint (warm → #1A1918, cool → #0F172A)
- LIGHT_BORDER: always ~1 shade darker than LIGHT_BG
- Brand gradient:
linear-gradient(165deg, BRAND_DARK 0%, BRAND_PRIMARY 50%, BRAND_LIGHT 100%)
Step 3: Set Up Typography
Based on the user's font preference, pick a heading font and body font from Google Fonts.
| Style | Heading Font | Body Font |
|---|---|---|
| Editorial / premium | Playfair Display | DM Sans |
| Modern / clean | Plus Jakarta Sans (700) | Plus Jakarta Sans (400) |
| Warm / approachable | Lora | Nunito Sans |
| Technical / sharp | Space Grotesk | Space Grotesk |
| Bold / expressive | Fraunces | Outfit |
| Classic / trustworthy | Libre Baskerville | Work Sans |
| Rounded / friendly | Bricolage Grotesque | Bricolage Grotesque |
Font size scale (fixed across all brands):
- Headings: 28–34px, weight 600, letter-spacing -0.3 to -0.5px, line-height 1.1–1.15
- Body: 14px, weight 400, line-height 1.5–1.55
- Tags/labels: 10px, weight 600, letter-spacing 2px, uppercase
- Step numbers: heading font, 26px, weight 300
- Small text: 11–12px
Apply via CSS classes .serif (heading font) and .sans (body font) throughout all slides.
Slide 1 — Hook Rules
The first slide must stop the scroll in under 1 second. Prioritize these formats:
| Hook format | Example |
|---|---|
| Afirmação polêmica | "Você está usando IA errado" |
| Número + benefício | "7 ferramentas que substituem seu designer" |
| Pergunta que dói | "Por que seus carrosséis têm 0 salvamentos?" |
| Resultado concreto | "Esse post gerou 4.200 seguidores em 3 dias" |
| Inversão de expectativa | "Mais esforço no design = menos alcance" |
Rules:
- Never start with the brand name as headline
- Visual proof on Slide 1 whenever possible (screenshot, result, real number)
- Hook must promise value that the following slides deliver
Slide Sequences
Standard (7 slides — default)
| # | Type | Background | Purpose |
|---|---|---|---|
| 1 | Hero | LIGHT_BG | Hook — bold statement, logo lockup, optional watermark |
| 2 | Problem | DARK_BG | Pain point — what's broken, frustrating, or outdated |
| 3 | Solution | Brand gradient | The answer — what solves it, optional quote/prompt box |
| 4 | Features | LIGHT_BG | What you get — feature list with icons |
| 5 | Details | DARK_BG | Depth — customization, specs, differentiators |
| 6 | How-to | LIGHT_BG | Steps — numbered workflow or process |
| 7 | CTA | Brand gradient | Call to action — logo, tagline, CTA button. No arrow. Full progress bar. |
Listicle (5–10 slides)
| # | Type | Background |
|---|---|---|
| 1 | Hero | LIGHT_BG |
| 2–N | Item N | Alternating LIGHT/DARK |
| Last | CTA | Brand gradient |
Use for: "X ferramentas", "X erros", "X dicas"
Tutorial (7 slides)
| # | Type | Background |
|---|---|---|
| 1 | Hero | LIGHT_BG |
| 2 | Contexto / Por quê | DARK_BG |
| 3–5 | Passo 1, 2, 3 | Alternating |
| 6 | Resultado esperado | DARK_BG |
| 7 | CTA | Brand gradient |
Comparação (5 slides)
| # | Type | Background |
|---|---|---|
| 1 | Hero (o que será comparado) | LIGHT_BG |
| 2 | Opção A | LIGHT_BG |
| 3 | Opção B | DARK_BG |
| 4 | Veredicto | Brand gradient |
| 5 | CTA | DARK_BG |
General rules for all sequences:
- Start with a hook — first slide must stop the scroll
- End CTA on brand gradient — no swipe arrow, progress bar at 100%
- Alternate light and dark backgrounds for visual rhythm
- Adapt sequence to topic — not every carousel needs all slides
Slide Architecture
Format
- Aspect ratio: 4:5 (Instagram carousel standard)
- Each slide is self-contained — all UI elements baked into the image
- Alternate LIGHT_BG and DARK_BG backgrounds for visual rhythm
Required Elements on Every Slide
1. Progress Bar (bottom of every slide)
Shows position in the carousel. Fills as user swipes.
- Position: absolute bottom, full width, 28px horizontal padding, 20px bottom padding
- Track: 3px height, rounded corners
- Fill width:
((slideIndex + 1) / totalSlides) * 100% - Light slides:
rgba(0,0,0,0.08)track,BRAND_PRIMARYfill,rgba(0,0,0,0.3)counter - Dark slides:
rgba(255,255,255,0.12)track,#ffffill,rgba(255,255,255,0.4)counter - Counter label beside the bar: "1/7" format, 11px, weight 500
function progressBar(index, total, isLightSlide) {
const pct = ((index + 1) / total) * 100;
const trackColor = isLightSlide ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.12)';
const fillColor = isLightSlide ? BRAND_PRIMARY : '#fff'; // use actual BRAND_PRIMARY value
const labelColor = isLightSlide ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.4)';
return `<div style="position:absolute;bottom:0;left:0;right:0;padding:16px 28px 20px;z-index:10;display:flex;align-items:center;gap:10px;">
<div style="flex:1;height:3px;background:${trackColor};border-radius:2px;overflow:hidden;">
<div style="height:100%;width:${pct}%;background:${fillColor};border-radius:2px;"></div>
</div>
<span style="font-size:11px;color:${labelColor};font-weight:500;">${index + 1}/${total}</span>
</div>`;
}
⚠️ Important: Always replace BRAND_PRIMARY with the actual hex value before rendering. Never leave it as a variable name in the HTML output.
2. Swipe Arrow (right edge — every slide EXCEPT the last)
Subtle chevron guiding the user to keep swiping. Removed on the last slide.
- Position: absolute right, full height, 48px wide
- Background: gradient fade transparent → subtle tint
- Chevron: 24×24 SVG, rounded strokes
- Light slides:
rgba(0,0,0,0.06)bg,rgba(0,0,0,0.25)stroke - Dark slides:
rgba(255,255,255,0.08)bg,rgba(255,255,255,0.35)stroke
function swipeArrow(isLightSlide) {
const bg = isLightSlide ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.08)';
const stroke = isLightSlide ? 'rgba(0,0,0,0.25)' : 'rgba(255,255,255,0.35)';
return `<div style="position:absolute;right:0;top:0;bottom:0;width:48px;z-index:9;display:flex;align-items:center;justify-content:center;background:linear-gradient(to right,transparent,${bg});">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 6l6 6-6 6" stroke="${stroke}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>`;
}
Reusable Components
Strikethrough pills
<span style="font-size:11px;padding:5px 12px;border:1px solid rgba(255,255,255,0.1);border-radius:20px;color:#6B6560;text-decoration:line-through;">{Old tool}</span>
Tag pills
<span style="font-size:11px;padding:5px 12px;background:rgba(255,255,255,0.06);border-radius:20px;color:{BRAND_LIGHT};">{Label}</span>
Prompt / quote box
<div style="padding:16px;background:rgba(0,0,0,0.15);border-radius:12px;border:1px solid rgba(255,255,255,0.08);">
<p class="sans" style="font-size:13px;color:rgba(255,255,255,0.5);margin-bottom:6px;">{Label}</p>
<p class="serif" style="font-size:15px;color:#fff;font-style:italic;line-height:1.4;">"{Quote text}"</p>
</div>
Feature list
<div style="display:flex;align-items:flex-start;gap:14px;padding:10px 0;border-bottom:1px solid {LIGHT_BORDER};">
<span style="color:{BRAND_PRIMARY};font-size:15px;width:18px;text-align:center;">{icon}</span>
<div>
<span class="sans" style="font-size:14px;font-weight:600;color:{DARK_BG};">{Label}</span>
<span class="sans" style="font-size:12px;color:#8A8580;">{Description}</span>
</div>
</div>
Numbered steps
<div style="display:flex;align-items:flex-start;gap:16px;padding:14px 0;border-bottom:1px solid {LIGHT_BORDER};">
<span class="serif" style="font-size:26px;font-weight:300;color:{BRAND_PRIMARY};min-width:34px;line-height:1;">01</span>
<div>
<span class="sans" style="font-size:14px;font-weight:600;color:{DARK_BG};">{Step title}</span>
<span class="sans" style="font-size:12px;color:#8A8580;">{Step description}</span>
</div>
</div>
Color swatches
<div style="width:32px;height:32px;border-radius:8px;background:{color};border:1px solid rgba(255,255,255,0.08);"></div>
CTA button (final slide only)
<div style="display:inline-flex;align-items:center;gap:8px;padding:12px 28px;background:{LIGHT_BG};color:{BRAND_DARK};font-family:'{BODY_FONT}',sans-serif;font-weight:600;font-size:14px;border-radius:28px;">
{CTA text}
</div>
Tag / Category Label
<span class="sans" style="display:inline-block;font-size:10px;font-weight:600;letter-spacing:2px;color:{color};margin-bottom:16px;">{TAG TEXT}</span>
- Light slides:
BRAND_PRIMARY - Dark slides:
BRAND_LIGHT - Brand gradient slides:
rgba(255,255,255,0.6)
Logo Lockup (first and last slides)
- If logo icon: 40px circle (BRAND_PRIMARY bg) + icon centered + brand name beside
- If initials: 40px circle with first letter in white
- Brand name: 13px, weight 600, letter-spacing 0.5px
Layout Rules
- Content padding:
0 36pxstandard - Bottom-aligned slides with progress bar:
0 36px 52pxto clear the bar - Hero/CTA slides:
justify-content: center - Content-heavy slides:
justify-content: flex-end - Content must never overlap the progress bar — use
padding-bottom: 52px
Instagram Frame (Preview Wrapper)
When displaying in chat, wrap in an Instagram-style frame:
- Header: Avatar (BRAND_PRIMARY circle + logo) + handle + subtitle
- Viewport: 4:5 aspect ratio, swipeable/draggable track with all slides
- Dots: Small dot indicators below the viewport
- Actions: Heart, comment, share, bookmark SVG icons
- Caption: Handle + short description + "2 HOURS AGO" timestamp
Include pointer-based swipe/drag interaction for preview. Slides are still standalone export-ready images.
Important: .ig-frame must be exactly 420px wide. The carousel viewport is 420×525px. Do NOT change this width — export depends on it.
Review Flow
Always follow this flow. Never skip to export without approval.
- Generate the HTML preview first — never jump directly to export
- Show the preview and ask: "Quais slides precisam de ajuste antes de exportar?"
- Fix only the mentioned slides — never regenerate the entire carousel unless the direction fundamentally changes
- Only proceed to export when the user explicitly confirms approval (e.g., "pode exportar", "aprovado", "ok")
Exporting Slides as Instagram-Ready PNGs
After the user approves the carousel preview, export each slide as an individual 1080×1350px PNG.
Critical Export Rules
-
Use Python for HTML generation — never use shell scripts with variable interpolation. Always use
Path.write_text()oropen().write(). -
Embed images as base64 — all user-uploaded images must be base64-encoded as
data:image/jpeg;base64,...URIs. Check actual file format with thefilecommand — a.pngextension may contain a JPEG. -
Keep the 420px layout width — use Playwright's
device_scale_factorto scale up to 1080px output WITHOUT changing the layout viewport.
Install Playwright (only if needed)
Before running the export script, check and install only if missing:
python3 -c "import playwright" 2>/dev/null || pip3 install playwright
python3 -c "from playwright.sync_api import sync_playwright; sync_playwright().__enter__().chromium" 2>/dev/null || python3 -m playwright install chromium
Export Script
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
INPUT_HTML = Path("/path/to/carousel.html")
OUTPUT_DIR = Path("/path/to/output/slides")
OUTPUT_DIR.mkdir(exist_ok=True)
TOTAL_SLIDES = 7 # Update to match your carousel
VIEW_W = 420
VIEW_H = 525
SCALE = 1080 / 420 # = 2.5714...
async def export_slides():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(
viewport={"width": VIEW_W, "height": VIEW_H},
device_scale_factor=SCALE,
)
html_content = INPUT_HTML.read_text(encoding="utf-8")
await page.set_content(html_content, wait_until="networkidle")
await page.wait_for_timeout(3000) # Wait for Google Fonts to load
# Hide IG frame chrome, show only the slide viewport
await page.evaluate("""() => {
document.querySelectorAll('.ig-header,.ig-dots,.ig-actions,.ig-caption')
.forEach(el => el.style.display='none');
const frame = document.querySelector('.ig-frame');
frame.style.cssText = 'width:420px;height:525px;max-width:none;border-radius:0;box-shadow:none;overflow:hidden;margin:0;';
const viewport = document.querySelector('.carousel-viewport');
viewport.style.cssText = 'width:420px;height:525px;aspect-ratio:unset;overflow:hidden;cursor:default;';
document.body.style.cssText = 'padding:0;margin:0;display:block;overflow:hidden;';
}""")
await page.wait_for_timeout(500)
for i in range(TOTAL_SLIDES):
await page.evaluate("""(idx) => {
const track = document.querySelector('.carousel-track');
track.style.transition = 'none';
track.style.transform = 'translateX(' + (-idx * 420) + 'px)';
}""", i)
await page.wait_for_timeout(400)
await page.screenshot(
path=str(OUTPUT_DIR / f"slide_{i+1}.png"),
clip={"x": 0, "y": 0, "width": VIEW_W, "height": VIEW_H}
)
print(f"Exported slide {i+1}/{TOTAL_SLIDES}")
await browser.close()
asyncio.run(export_slides())
Why This Works
device_scale_factor=2.5714renders at high DPI — a 420px element becomes 1080px in the output. Layout stays at 420px.clipcaptures only the carousel viewport, not browser chrome.wait_for_timeout(3000)gives Google Fonts time to load.track.style.transition = 'none'disables swipe animation so slides snap instantly.
Common Export Mistakes to Avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
| Setting viewport to 1080×1350 | Layout reflows — fonts tiny, spacing breaks | Keep viewport at 420×525, use device_scale_factor |
| Using shell scripts to generate HTML | $ signs and backticks get interpolated | Always use Python for HTML generation |
| Not waiting for fonts | Headings render in fallback system fonts | wait_for_timeout(3000) after page load |
| Not hiding IG frame chrome | Export includes header, dots, caption | Hide .ig-header,.ig-dots,.ig-actions,.ig-caption |
Changing .ig-frame width | Entire layout shifts | Always keep at exactly 420px |
Leaving BRAND_PRIMARY as variable name in CSS | Color renders as invalid / invisible | Always interpolate actual hex values into HTML |
Design Principles
- Every slide is export-ready — arrow and progress bar are part of the slide image
- Light/dark alternation — creates visual rhythm across swipes
- Heading + body font pairing — display font for impact, body for readability
- Brand-derived palette — all colors stem from one primary, keeping everything cohesive
- Progressive disclosure — progress bar fills and arrow guides forward
- Last slide is special — no arrow, full progress bar, clear CTA
- Consistent components — same tag style, list style, spacing across all slides
- Content padding clears UI — body text never overlaps progress bar or arrow
- Hook-first copy — Slide 1 exists to stop the scroll, not to introduce the brand
- Iterate fast — show preview, fix specific slides, don't rebuild from scratch
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/marcolang/marketing-skills/instagram-carousel">View instagram-carousel on skillZs</a>