StealThis .dev
Remotion Medium

YouTube Shorts Template (Remotion)

A 30-second vertical YouTube Shorts template with animated title card, hook text, subscribe button animation, and progress bar — 1080×1920, 30 fps.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import React from "react";
import {
  AbsoluteFill,
  Composition,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

// ── CONFIG ────────────────────────────────────────────────────────────────────
const DURATION = 900; // 30 s at 30 fps

const CHANNEL_NAME = "@TechShorts";
const CHANNEL_SUBS = "142K subscribers";

const HOOK_WORDS = [
  "This",
  "Will",
  "Change",
  "How",
  "You",
  "Code",
  "Forever",
];

const BG_FROM = "#0f0f0f";
const BG_TO = "#1a1a1a";
const ACCENT = "#ff0000"; // YouTube red
const TEXT_PRIMARY = "#ffffff";
const TEXT_MUTED = "rgba(255,255,255,0.60)";

const FONT = "system-ui, -apple-system, 'Helvetica Neue', sans-serif";

// ── Dot-grid overlay ──────────────────────────────────────────────────────────
const GridOverlay: React.FC = () => (
  <div
    style={{
      position: "absolute",
      inset: 0,
      backgroundImage:
        "radial-gradient(circle, rgba(255,255,255,0.07) 1px, transparent 1px)",
      backgroundSize: "40px 40px",
      pointerEvents: "none",
    }}
  />
);

// ── Progress bar ──────────────────────────────────────────────────────────────
const ProgressBar: React.FC<{ frame: number }> = ({ frame }) => {
  const progress = interpolate(frame, [0, DURATION], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        height: 3,
        backgroundColor: "rgba(255,255,255,0.18)",
      }}
    >
      <div
        style={{
          width: `${progress * 100}%`,
          height: "100%",
          backgroundColor: ACCENT,
        }}
      />
    </div>
  );
};

// ── Channel header (top-left) ─────────────────────────────────────────────────
const ChannelHeader: React.FC<{ frame: number; fps: number }> = ({
  frame,
  fps,
}) => {
  const opacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const translateY = spring({
    frame,
    fps,
    from: -24,
    to: 0,
    config: { damping: 18, stiffness: 120 },
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 64,
        left: 28,
        display: "flex",
        alignItems: "center",
        gap: 14,
        opacity,
        transform: `translateY(${translateY}px)`,
      }}
    >
      {/* Avatar circle */}
      <div
        style={{
          width: 52,
          height: 52,
          borderRadius: "50%",
          background: "linear-gradient(135deg, #ff4e50, #fc913a)",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: 22,
          flexShrink: 0,
          border: "2px solid rgba(255,255,255,0.20)",
        }}
      >

      </div>

      {/* Name + subs */}
      <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 700,
            fontSize: 18,
            color: TEXT_PRIMARY,
            lineHeight: 1,
          }}
        >
          {CHANNEL_NAME}
        </div>
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 400,
            fontSize: 13,
            color: TEXT_MUTED,
          }}
        >
          {CHANNEL_SUBS}
        </div>
      </div>
    </div>
  );
};

// ── Subscribe button ──────────────────────────────────────────────────────────
const SubscribeButton: React.FC<{ frame: number; fps: number }> = ({
  frame,
  fps,
}) => {
  // Bounces in at frame 60
  const f = Math.max(0, frame - 60);
  const scale = spring({
    frame: f,
    fps,
    from: 0,
    to: 1,
    config: { damping: 10, stiffness: 180, mass: 0.6 },
  });
  const opacity = interpolate(f, [0, 8], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 68,
        right: 28,
        opacity,
        transform: `scale(${scale})`,
        transformOrigin: "center right",
      }}
    >
      <div
        style={{
          backgroundColor: ACCENT,
          borderRadius: 24,
          paddingTop: 10,
          paddingBottom: 10,
          paddingLeft: 22,
          paddingRight: 22,
          display: "flex",
          alignItems: "center",
          gap: 6,
          boxShadow: "0 0 20px rgba(255,0,0,0.45)",
        }}
      >
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 700,
            fontSize: 15,
            color: TEXT_PRIMARY,
            letterSpacing: 0.3,
          }}
        >
          Subscribe
        </div>
      </div>
    </div>
  );
};

// ── Like / Dislike icons (right side) ─────────────────────────────────────────
const LikeDislike: React.FC<{ frame: number; fps: number }> = ({
  frame,
  fps,
}) => {
  // Slide in from the right at frame 30
  const f = Math.max(0, frame - 30);
  const translateX = spring({
    frame: f,
    fps,
    from: 80,
    to: 0,
    config: { damping: 16, stiffness: 100 },
  });
  const opacity = interpolate(f, [0, 12], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const iconStyle: React.CSSProperties = {
    width: 56,
    height: 56,
    borderRadius: "50%",
    backgroundColor: "rgba(255,255,255,0.10)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontSize: 26,
    border: "1.5px solid rgba(255,255,255,0.14)",
  };

  const labelStyle: React.CSSProperties = {
    fontFamily: FONT,
    fontWeight: 600,
    fontSize: 13,
    color: TEXT_MUTED,
    textAlign: "center" as const,
    marginTop: 4,
  };

  return (
    <div
      style={{
        position: "absolute",
        right: 20,
        bottom: 220,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: 20,
        opacity,
        transform: `translateX(${translateX}px)`,
      }}
    >
      {/* Like */}
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
        <div style={iconStyle}>👍</div>
        <div style={labelStyle}>47K</div>
      </div>

      {/* Dislike */}
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
        <div style={iconStyle}>👎</div>
        <div style={labelStyle}>Dislike</div>
      </div>
    </div>
  );
};

// ── Hook text (word-by-word stagger) ──────────────────────────────────────────
const HookText: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  // Overall container fades in gently
  const containerOpacity = interpolate(frame, [5, 25], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        bottom: 120,
        left: 28,
        right: 100,
        opacity: containerOpacity,
      }}
    >
      {/* Label */}
      <div
        style={{
          fontFamily: FONT,
          fontWeight: 500,
          fontSize: 13,
          color: TEXT_MUTED,
          letterSpacing: 1.5,
          textTransform: "uppercase" as const,
          marginBottom: 10,
        }}
      >
        Today's tip
      </div>

      {/* Staggered words */}
      <div style={{ display: "flex", flexWrap: "wrap" as const, gap: "0 10px", rowGap: 4 }}>
        {HOOK_WORDS.map((word, i) => {
          const startFrame = 10 + i * 7;
          const f = Math.max(0, frame - startFrame);

          const wordOpacity = interpolate(f, [0, 12], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });

          const translateY = spring({
            frame: f,
            fps,
            from: 40,
            to: 0,
            config: { damping: 14, stiffness: 160 },
          });

          return (
            <span
              key={i}
              style={{
                opacity: wordOpacity,
                transform: `translateY(${translateY}px)`,
                display: "inline-block",
                fontFamily: FONT,
                fontWeight: 900,
                fontSize: 58,
                lineHeight: 1.08,
                color: TEXT_PRIMARY,
                textShadow: "0 2px 12px rgba(0,0,0,0.7)",
              }}
            >
              {word}
            </span>
          );
        })}
      </div>
    </div>
  );
};

// ── Center content placeholder ────────────────────────────────────────────────
const ContentArea: React.FC<{ frame: number }> = ({ frame }) => {
  const opacity = interpolate(frame, [0, 30], [0, 0.08], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        opacity,
      }}
    >
      <div
        style={{
          fontFamily: FONT,
          fontSize: 16,
          color: TEXT_PRIMARY,
          letterSpacing: 3,
          textTransform: "uppercase" as const,
        }}
      >
        Video Content
      </div>
    </div>
  );
};

// ── Shorts-style top bar (title + close icon) ─────────────────────────────────
const ShortsTopBar: React.FC<{ frame: number }> = ({ frame }) => {
  const opacity = interpolate(frame, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        height: 52,
        background:
          "linear-gradient(180deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 100%)",
        display: "flex",
        alignItems: "center",
        paddingLeft: 20,
        paddingRight: 20,
        opacity,
      }}
    >
      <div
        style={{
          fontFamily: FONT,
          fontWeight: 700,
          fontSize: 16,
          color: TEXT_PRIMARY,
          letterSpacing: 0.5,
          flexGrow: 1,
        }}
      >
        Shorts
      </div>
      <div
        style={{
          fontFamily: FONT,
          fontSize: 22,
          color: TEXT_PRIMARY,
          opacity: 0.7,
        }}
      >

      </div>
    </div>
  );
};

// ── Main composition ───────────────────────────────────────────────────────────
export const YouTubeShorts: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill
      style={{
        background: `linear-gradient(180deg, ${BG_FROM} 0%, ${BG_TO} 100%)`,
        overflow: "hidden",
      }}
    >
      <GridOverlay />
      <ContentArea frame={frame} />
      <ShortsTopBar frame={frame} />
      <ChannelHeader frame={frame} fps={fps} />
      <SubscribeButton frame={frame} fps={fps} />
      <LikeDislike frame={frame} fps={fps} />
      <HookText frame={frame} fps={fps} />
      <ProgressBar frame={frame} />
    </AbsoluteFill>
  );
};

// ── Remotion Root ─────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
  <Composition
    id="YouTubeShorts"
    component={YouTubeShorts}
    durationInFrames={DURATION}
    fps={30}
    width={1080}
    height={1920}
  />
);

YouTube Shorts Template

A 30-second vertical YouTube Shorts composition (1080×1920, 30 fps) that mimics the native Shorts in-app UI. Hook text words stagger in from the bottom one by one with spring physics, the channel avatar and name fade in at the top, a red pill subscribe button bounces into frame at second 2, like and dislike icons slide in from the right at second 1, and a thin red progress bar tracks playback at the bottom edge — all on a dark grid-overlay background that keeps the focus on the content.

Composition specs

PropertyValue
Resolution1080 × 1920
FPS30
Duration30 s (900 frames)

Elements

  • Dark gradient background (#0f0f0f#1a1a1a) with a subtle dot-grid overlay
  • Hook text — “This Will Change How You Code Forever” — words stagger in from below with spring entrances
  • Channel header (top-left): avatar circle, channel name @TechShorts, subscriber count
  • Subscribe button — red pill, bounces in at frame 60 with spring overshoot
  • Like / Dislike icons — slide in from the right at frame 30
  • Progress bar — thin red bar at the bottom that fills over the full 900-frame duration