Back to library

Social Clips

Turn Slack threads into animated social videos with realistic UI details (avatars, typing indicators, reactions, spring animations) and render to MP4/GIF in vertical and horizontal formats.

Install to Plus One Download ZIP

Package

Skill files

Files
src
assets
avatars
components
slack
data
lib

SKILL.md

markdown

Raw
name: social-clips
description: >
  Turn Slack threads into animated social videos with realistic UI details (avatars,
  typing indicators, reactions, spring animations) and render to MP4/GIF in vertical
  and horizontal formats.
tags: [video, slack, social, remotion]
version: 1.0.0

Social Clips

Turn Slack threads into animated social videos. Slack dark mode with real profile photos, typing indicators, reactions, and spring animations.

Outputs: MP4 (vertical + horizontal) and GIF.

Quick Start

npm install
npm run studio           # preview in browser
npm run render:stories   # 1080x1920 MP4
npm run render:landscape # 1920x1080 MP4
npm run gif:stories      # 1080x1920 GIF
npm run gif:landscape    # 1920x1080 GIF

Or render any composition directly:

npx remotion render <composition-id> out/<name>.mp4 --codec=h264 --crf=18

Making a New Clip

1. Pull the Slack thread

mcp__slack__slack_get_thread_replies(channel_id, thread_ts)

Extract thread_ts from the URL: p1234567890123456 → 1234567890.123456

2. Get avatar photos

mcp__slack__slack_get_users(limit: 200)

Download image_512 URLs into src/assets/avatars/:

curl -sL -o src/assets/avatars/name.jpg "https://avatars.slack-edge.com/..."

3. Add senders

In src/slack-types.ts:

  1. Add to the SlackSender union type
  2. Import the avatar image
  3. Add a SenderConfig entry with avatarPhoto

The avatar component renders the photo when available, falls back to colored initials.

4. Write the data file

Create src/data/<clip-name>.ts:

import type { SlackMessage, SlackTimedEvent } from '../slack-types';

export const MESSAGES: SlackMessage[] = [
  { id: 0, sender: 'dan', text: 'Opening message' },
  { id: 1, sender: 'r2c2', text: 'Reply with *bold* and @mentions' },
  { id: 2, sender: 'austin', text: 'Another message', reactions: [{ emoji: '🔥', count: 3 }] },
];

export const TIMELINE: SlackTimedEvent[] = [
  // Messages
  { type: 'message', messageIndex: 0, startFrame: 30, durationFrames: 40 },

  // Typing indicator before a reply
  { type: 'typing', typingSender: 'r2c2', startFrame: 75, durationFrames: 40 },
  { type: 'message', messageIndex: 1, startFrame: 115, durationFrames: 40 },

  // Human messages just appear (no typing indicator)
  { type: 'message', messageIndex: 2, startFrame: 165, durationFrames: 40 },

  // Reaction pops in after a message
  { type: 'reaction', messageIndex: 2, reactionIndex: 0, startFrame: 215, durationFrames: 20 },

  // Pause for tension
  { type: 'pause', typingSender: 'dan', startFrame: 240, durationFrames: 60 },
];

export const TOTAL_FRAMES = 1800; // 60s at 30fps
export const FPS = 30;

Text supports: @mentions, *bold*, \n newlines, • bullets

Consecutive messages from the same sender collapse the avatar + name automatically.

5. Register the composition

In src/Root.tsx:

import { MESSAGES, TIMELINE, TOTAL_FRAMES, FPS } from './data/my-clip';

// Vertical
<Composition
  id="my-clip-stories"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1080}
  height={1920}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'portrait' }}
/>

// Horizontal
<Composition
  id="my-clip-landscape"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1920}
  height={1080}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'landscape' }}
/>

6. Render

npx remotion render my-clip-stories out/my-clip-stories.mp4 --codec=h264 --crf=18
npx remotion render my-clip-landscape out/my-clip-landscape.mp4 --codec=h264 --crf=18

GIF conversion:

# Vertical
ffmpeg -y -i out/my-clip-stories.mp4 \
  -vf "fps=15,scale=540:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-stories.gif

# Horizontal
ffmpeg -y -i out/my-clip-landscape.mp4 \
  -vf "fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-landscape.gif

Narrative Arc

Find the spine of any thread:

| Beat | Msgs | Look for | |------|------|----------| | Hook | 1-2 | The inciting question | | Brainstorm | 3-6 | Ideas flying, agents riffing | | Conflict | 2-3 | Challenge, wrong turn, pushback | | Breakthrough | 1-2 | The idea that lights everyone up | | Eruption | 3-5 | Pile-on, excitement, reactions | | Close | 1-2 | The line that crystallizes it |

Rules:

  • ~80 words max per message
  • 15-21 messages for 60-75s
  • Agents get typing indicators, humans don't
  • Put the longest pause before the breakthrough
  • Eruption = fast pile-up (20-30 frame gaps)
  • Final hold: 7+ seconds

Timeline Reference

30fps. 30 frames = 1 second.

| Duration | Frames | Messages | |----------|--------|----------| | 60s | 1800 | 15-17 | | 75s | 2250 | 18-21 | | 90s | 2700 | 22-25 |

| Event | Frames | Notes | |-------|--------|-------| | Short message | 25-35 | ~1s read | | Long message | 45-60 | ~2s read | | Typing (fast) | 25-35 | Agent is quick | | Typing (thinking) | 45-55 | Agent is deliberating | | Brief pause | 20-40 | Beat | | Big pause | 80-120 | Before breakthrough | | Reaction | 20 | Quick pop | | Final hold | 200-360 | Let it breathe |

Components

| File | What | |------|------| | SlackScreen | Main composition — header, messages, typing, input bar | | SlackMessageRow | Avatar, name, APP badge, text, reactions | | SlackAvatar | Photo with colored-initial fallback | | SlackHeader | "Thread" header with channel name (configurable) | | SlackTypingIndicator | Animated dots with sender name | | SlackReactionPill | Emoji + count pill | | SlackInputBar | Input field chrome |

Types

type SlackSender = string;  // extend union in slack-types.ts

interface SlackMessage {
  id: number;
  text: string;
  sender: SlackSender;
  reactions?: Array<{ emoji: string; count: number }>;
}

interface SlackTimedEvent {
  type: 'message' | 'typing' | 'reaction' | 'pause';
  messageIndex?: number;
  reactionIndex?: number;
  typingSender?: SlackSender;
  startFrame: number;
  durationFrames: number;
}

interface SenderConfig {
  name: string;
  initials: string;
  avatarColor: string;
  isApp: boolean;
  avatarPhoto?: string;  // imported image path
}

Existing Clips

| ID | Size | Content | |----|------|---------| | plus-one-slack-stories | 1080x1920 | Plus One naming (75s) | | plus-one-slack-landscape | 1920x1080 | Plus One naming (75s) |

Related