StealThis .dev

Byline / Credit Animation (Remotion)

A broadcast-quality Remotion composition that cycles through three professional credit styles at 1280x720 30fps. Scene 1 springs a reporter byline with a red pill badge onto the bottom-left via a stiff spring. Scene 2 fades a minimal photographer overlay into the top-right corner with a scrim card. Scene 3 slides up a full-width production credit band with letter-by-letter text reveals and a network logo stamp — all across 120 frames of frame-accurate animation.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

// ─── Customizable constants ────────────────────────────────────────────────
const NETWORK_NAME = "NNX";
const NETWORK_TAGLINE = "NNX Digital";

const CREDITS = [
  { role: "REPORTER", name: "Marcus Webb", detail: "NNX Washington Bureau" },
  { role: "PHOTOGRAPHER", name: "© NNX / Sarah Kim", detail: "Visual Correspondent" },
  {
    role: "PRODUCTION",
    name: "NNX DIGITAL",
    producers: [
      { label: "PRODUCED BY", value: "NNX Digital" },
      { label: "DIRECTED BY", value: "Carlos Reyes" },
      { label: "EDITED BY", value: "Priya Sharma" },
    ],
  },
];

const ACCENT_RED = "#e8001e";
const ACCENT_GOLD = "#f5c842";
const BG = "#0d1117";
const WHITE = "#ffffff";
const OFF_WHITE = "rgba(255,255,255,0.85)";
const MUTED = "rgba(255,255,255,0.50)";
const SUBTLE = "rgba(255,255,255,0.18)";
const GRID_LINE = "rgba(255,255,255,0.04)";
const SCRIM_DARK = "rgba(0,0,0,0.72)";
const BAND_BG = "rgba(10,12,20,0.96)";

const FONT = "Inter, system-ui, -apple-system, sans-serif";

// ─── Scene boundaries ──────────────────────────────────────────────────────
// Scene 1:   0 – 40  Reporter byline — bottom-left, red pill badge, spring-in/hold/spring-out
// Scene 2:  40 – 80  Photographer credit — top-right corner overlay, scrim, fade in/hold/fade out
// Scene 3:  80 – 120 Full production strip — bottom band, letter-by-letter fade, network logo right

// ─── Utility ───────────────────────────────────────────────────────────────
function clamp(v: number, lo: number, hi: number) {
  return Math.max(lo, Math.min(hi, v));
}

// ─── Background ─────────────────────────────────────────────────────────────
const BackgroundGrid: React.FC = () => (
  <>
    <div
      style={{
        position: "absolute",
        inset: 0,
        background: `radial-gradient(ellipse 900px 500px at 50% 60%, rgba(232,0,30,0.06) 0%, transparent 70%), ${BG}`,
      }}
    />
    {Array.from({ length: 13 }).map((_, i) => (
      <div
        key={`col-${i}`}
        style={{
          position: "absolute",
          top: 0,
          bottom: 0,
          left: `${(i / 12) * 100}%`,
          width: 1,
          backgroundColor: GRID_LINE,
        }}
      />
    ))}
    {Array.from({ length: 8 }).map((_, i) => (
      <div
        key={`row-${i}`}
        style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: `${(i / 7) * 100}%`,
          height: 1,
          backgroundColor: GRID_LINE,
        }}
      />
    ))}
  </>
);

// Placeholder "on-air" talking head area — blurred dark vignette center
const TalentArea: React.FC = () => (
  <div
    style={{
      position: "absolute",
      inset: 0,
      background:
        "radial-gradient(ellipse 700px 600px at 50% 45%, rgba(30,38,60,0.55) 0%, transparent 80%)",
    }}
  />
);

// ─── Scene 1: Reporter Byline ────────────────────────────────────────────────
interface Scene1Props {
  frame: number;
  fps: number;
}

const RedPillBadge: React.FC<{ label: string }> = ({ label }) => (
  <div
    style={{
      display: "inline-flex",
      alignItems: "center",
      backgroundColor: ACCENT_RED,
      borderRadius: 3,
      paddingLeft: 10,
      paddingRight: 10,
      paddingTop: 4,
      paddingBottom: 4,
      marginBottom: 8,
    }}
  >
    <span
      style={{
        fontFamily: FONT,
        fontWeight: 800,
        fontSize: 11,
        letterSpacing: 2,
        color: WHITE,
        textTransform: "uppercase" as const,
      }}
    >
      {label}
    </span>
  </div>
);

const Scene1ReporterByline: React.FC<Scene1Props> = ({ frame, fps }) => {
  // Slide in: frames 0–12
  const slideIn = spring({
    frame: clamp(frame, 0, 12),
    fps,
    config: { damping: 20, stiffness: 180 },
  });

  // Slide out: frames 28–40
  const slideOutProgress = interpolate(frame, [28, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.in(Easing.quad),
  });

  const translateX = interpolate(slideIn, [0, 1], [-420, 0]) - slideOutProgress * 420;
  const opacity = interpolate(slideIn, [0, 1], [0, 1]) * (1 - slideOutProgress * 0.4);

  const reporter = CREDITS[0];

  return (
    <div
      style={{
        position: "absolute",
        bottom: 80,
        left: 56,
        transform: `translateX(${translateX}px)`,
        opacity,
      }}
    >
      {/* Dark scrim behind the byline block */}
      <div
        style={{
          position: "absolute",
          inset: "-12px -20px -12px -20px",
          backgroundColor: SCRIM_DARK,
          borderRadius: 4,
          borderLeft: `4px solid ${ACCENT_RED}`,
        }}
      />
      <div style={{ position: "relative" }}>
        <RedPillBadge label={reporter.role} />
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 800,
            fontSize: 32,
            color: WHITE,
            lineHeight: 1.1,
            letterSpacing: 0.3,
            whiteSpace: "nowrap" as const,
          }}
        >
          {reporter.name}
        </div>
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 400,
            fontSize: 15,
            color: MUTED,
            marginTop: 5,
            letterSpacing: 0.5,
          }}
        >
          {reporter.detail}
        </div>
      </div>
    </div>
  );
};

// ─── Scene 2: Photographer Credit (top-right) ────────────────────────────────
interface Scene2Props {
  frame: number;
}

const Scene2PhotographerCredit: React.FC<Scene2Props> = ({ frame }) => {
  // Active window: 40–80
  const localFrame = frame - 40;

  // Fade in: local 0–10
  const fadeIn = interpolate(localFrame, [0, 10], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Fade out: local 28–40
  const fadeOut = interpolate(localFrame, [28, 40], [1, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.in(Easing.cubic),
  });

  const opacity = Math.min(fadeIn, fadeOut);

  // Subtle float-down: slides from -8px to 0 on fade-in
  const translateY = interpolate(fadeIn, [0, 1], [-10, 0]);

  const photographer = CREDITS[1];

  if (frame < 40 || frame > 80) return null;

  return (
    <div
      style={{
        position: "absolute",
        top: 52,
        right: 52,
        opacity,
        transform: `translateY(${translateY}px)`,
        display: "flex",
        flexDirection: "column" as const,
        alignItems: "flex-end",
      }}
    >
      {/* scrim card */}
      <div
        style={{
          backgroundColor: SCRIM_DARK,
          borderRadius: 4,
          padding: "12px 18px",
          borderTop: `2px solid ${SUBTLE}`,
          backdropFilter: "blur(4px)",
        }}
      >
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 400,
            fontSize: 11,
            color: MUTED,
            letterSpacing: 2,
            textTransform: "uppercase" as const,
            marginBottom: 5,
            textAlign: "right" as const,
          }}
        >
          Photo
        </div>
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 700,
            fontSize: 18,
            color: WHITE,
            letterSpacing: 0.2,
            whiteSpace: "nowrap" as const,
            textAlign: "right" as const,
          }}
        >
          {photographer.name}
        </div>
        <div
          style={{
            fontFamily: FONT,
            fontWeight: 400,
            fontSize: 12,
            color: MUTED,
            marginTop: 3,
            letterSpacing: 0.4,
            textAlign: "right" as const,
          }}
        >
          {photographer.detail}
        </div>
      </div>

      {/* decorative corner accent */}
      <div
        style={{
          width: 28,
          height: 2,
          backgroundColor: ACCENT_GOLD,
          marginTop: 6,
          alignSelf: "flex-end",
          opacity: opacity,
        }}
      />
    </div>
  );
};

// ─── Scene 3: Full Production Credit Strip ────────────────────────────────────
interface Scene3Props {
  frame: number;
  fps: number;
}

interface CreditEntry {
  label: string;
  value: string;
}

// Each character in a credit item fades in sequentially
const LetterFadeText: React.FC<{
  text: string;
  startFrame: number;
  frame: number;
  speed?: number;
  style?: React.CSSProperties;
}> = ({ text, startFrame, frame, speed = 1.2, style = {} }) => {
  return (
    <span style={{ display: "inline-flex", ...style }}>
      {text.split("").map((char, i) => {
        const charFrame = startFrame + i * speed;
        const alpha = interpolate(frame, [charFrame, charFrame + 4], [0, 1], {
          extrapolateLeft: "clamp",
          extrapolateRight: "clamp",
        });
        return (
          <span key={i} style={{ opacity: alpha, whiteSpace: "pre" as const }}>
            {char}
          </span>
        );
      })}
    </span>
  );
};

// Network logo mark — circle + letters
const NetworkLogo: React.FC<{ opacity: number }> = ({ opacity }) => (
  <div
    style={{
      display: "flex",
      alignItems: "center",
      gap: 10,
      opacity,
    }}
  >
    <div
      style={{
        width: 44,
        height: 44,
        borderRadius: "50%",
        backgroundColor: ACCENT_RED,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        flexShrink: 0,
      }}
    >
      <span
        style={{
          fontFamily: FONT,
          fontWeight: 900,
          fontSize: 16,
          color: WHITE,
          letterSpacing: -1,
        }}
      >
        {NETWORK_NAME}
      </span>
    </div>
    <div>
      <div
        style={{
          fontFamily: FONT,
          fontWeight: 700,
          fontSize: 13,
          color: WHITE,
          letterSpacing: 1.5,
        }}
      >
        {NETWORK_TAGLINE.toUpperCase()}
      </div>
      <div
        style={{
          fontFamily: FONT,
          fontWeight: 400,
          fontSize: 10,
          color: MUTED,
          letterSpacing: 1,
          marginTop: 2,
        }}
      >
        BROADCAST SERVICES
      </div>
    </div>
  </div>
);

const Scene3ProductionStrip: React.FC<Scene3Props> = ({ frame, fps }) => {
  // Active window: 80–120
  const localFrame = frame - 80;

  // Band slides up from below: spring on localFrame 0–18
  const bandSlide = spring({
    frame: clamp(localFrame, 0, 18),
    fps,
    config: { damping: 24, stiffness: 140 },
  });
  const bandY = interpolate(bandSlide, [0, 1], [90, 0]);

  // Band fades out at very end: local 36–40
  const bandFadeOut = interpolate(localFrame, [36, 40], [1, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Logo springs in after band: localFrame 14 onwards
  const logoSpring = spring({
    frame: clamp(localFrame - 14, 0, 20),
    fps,
    config: { damping: 22, stiffness: 160 },
  });
  const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]) * bandFadeOut;

  // Separator line scales in: localFrame 8–20
  const lineScale = interpolate(localFrame, [8, 22], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Credits start appearing at localFrame 10
  const production = CREDITS[2];
  const producers: CreditEntry[] = (production.producers as CreditEntry[]) ?? [];

  if (frame < 80) return null;

  return (
    <div
      style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        transform: `translateY(${bandY}px)`,
        opacity: bandFadeOut,
      }}
    >
      {/* Main dark band */}
      <div
        style={{
          backgroundColor: BAND_BG,
          borderTop: `1px solid rgba(255,255,255,0.10)`,
          paddingTop: 16,
          paddingBottom: 16,
          paddingLeft: 56,
          paddingRight: 56,
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          minHeight: 82,
          position: "relative",
        }}
      >
        {/* Red accent line at top of band */}
        <div
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            height: 3,
            width: `${lineScale * 100}%`,
            backgroundColor: ACCENT_RED,
          }}
        />

        {/* Credit items left-to-right */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 0,
            flex: 1,
          }}
        >
          {producers.map((item, idx) => {
            // Stagger start frames: first credit at local 10, each subsequent +18
            const creditStart = 10 + idx * 18;

            return (
              <div
                key={idx}
                style={{
                  display: "flex",
                  alignItems: "center",
                }}
              >
                <div>
                  <div
                    style={{
                      fontFamily: FONT,
                      fontWeight: 400,
                      fontSize: 10,
                      color: MUTED,
                      letterSpacing: 2,
                      textTransform: "uppercase" as const,
                      marginBottom: 4,
                    }}
                  >
                    <LetterFadeText
                      text={item.label}
                      startFrame={creditStart}
                      frame={localFrame}
                      speed={0.8}
                    />
                  </div>
                  <div
                    style={{
                      fontFamily: FONT,
                      fontWeight: 700,
                      fontSize: 17,
                      color: WHITE,
                      letterSpacing: 0.4,
                      whiteSpace: "nowrap" as const,
                    }}
                  >
                    <LetterFadeText
                      text={item.value}
                      startFrame={creditStart + 5}
                      frame={localFrame}
                      speed={1.0}
                    />
                  </div>
                </div>

                {/* Divider dot between items */}
                {idx < producers.length - 1 && (
                  <div
                    style={{
                      width: 4,
                      height: 4,
                      borderRadius: "50%",
                      backgroundColor: ACCENT_RED,
                      marginLeft: 28,
                      marginRight: 28,
                      flexShrink: 0,
                      opacity: interpolate(localFrame, [creditStart + 14, creditStart + 20], [0, 1], {
                        extrapolateLeft: "clamp",
                        extrapolateRight: "clamp",
                      }),
                    }}
                  />
                )}
              </div>
            );
          })}
        </div>

        {/* Vertical separator */}
        <div
          style={{
            width: 1,
            height: 44,
            backgroundColor: SUBTLE,
            marginLeft: 32,
            marginRight: 32,
            flexShrink: 0,
            opacity: lineScale,
          }}
        />

        {/* Network logo on far right */}
        <NetworkLogo opacity={logoOpacity} />
      </div>
    </div>
  );
};

// ─── Ambient scan line overlay ───────────────────────────────────────────────
const ScanLines: React.FC<{ frame: number }> = ({ frame }) => {
  // Slowly scrolling subtle scan line
  const offset = (frame * 2) % 60;
  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        backgroundImage: `repeating-linear-gradient(
          to bottom,
          transparent,
          transparent 3px,
          rgba(0,0,0,0.07) 3px,
          rgba(0,0,0,0.07) 4px
        )`,
        backgroundPositionY: `${offset}px`,
        pointerEvents: "none",
      }}
    />
  );
};

// ─── Timecode / Broadcast HUD ────────────────────────────────────────────────
const BroadcastHUD: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const totalSeconds = frame / fps;
  const mins = Math.floor(totalSeconds / 60)
    .toString()
    .padStart(2, "0");
  const secs = Math.floor(totalSeconds % 60)
    .toString()
    .padStart(2, "0");
  const centis = Math.floor((totalSeconds % 1) * 100)
    .toString()
    .padStart(2, "0");

  return (
    <div
      style={{
        position: "absolute",
        top: 16,
        left: 20,
        fontFamily: "monospace",
        fontSize: 11,
        color: SUBTLE,
        letterSpacing: 1.5,
        userSelect: "none",
      }}
    >
      {`TC ${mins}:${secs}:${centis}`}
    </div>
  );
};

// ─── Scene 1 background vignette hint ───────────────────────────────────────
const BottomGradientHint: React.FC<{ opacity: number }> = ({ opacity }) => (
  <div
    style={{
      position: "absolute",
      bottom: 0,
      left: 0,
      right: 0,
      height: 220,
      background: "linear-gradient(to top, rgba(0,0,0,0.65) 0%, transparent 100%)",
      opacity,
      pointerEvents: "none",
    }}
  />
);

// ─── Main composition ────────────────────────────────────────────────────────
export default function BylineCreditAnimation() {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Gradient at bottom for scenes 1–2 context
  const bottomVigOpacity = interpolate(frame, [76, 84], [1, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Global opacity for entire comp (fade in at 0–3, always full after)
  const globalOpacity = interpolate(frame, [0, 3], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill
      style={{
        backgroundColor: BG,
        fontFamily: FONT,
        overflow: "hidden",
        opacity: globalOpacity,
      }}
    >
      {/* Layer 1: Background canvas */}
      <BackgroundGrid />
      <TalentArea />
      <ScanLines frame={frame} />

      {/* Bottom-of-screen vignette (helps legibility for S1 & S2) */}
      <BottomGradientHint opacity={bottomVigOpacity} />

      {/* Scene 1: Reporter Byline (frames 0–40) */}
      {frame <= 42 && <Scene1ReporterByline frame={frame} fps={fps} />}

      {/* Scene 2: Photographer Credit (frames 40–80) */}
      {frame >= 38 && frame <= 82 && <Scene2PhotographerCredit frame={frame} />}

      {/* Scene 3: Full production credit strip (frames 80–120) */}
      {frame >= 78 && <Scene3ProductionStrip frame={frame} fps={fps} />}

      {/* HUD overlay: timecode (always visible, very subtle) */}
      <BroadcastHUD frame={frame} fps={fps} />
    </AbsoluteFill>
  );
}

Byline & Credit Animation

This Remotion composition demonstrates three professional on-screen credit treatments used in broadcast journalism, packaged into a single 4-second (120-frame) sequence at 1280×720, 30 fps. Each scene targets a different visual language — from a punchy reporter byline to a quiet photographer overlay to a cinematic production strip — making it straightforward to extract or adapt any single style for standalone use.

Scene 1 (frames 0–40) renders a reporter byline in the bottom-left corner. A solid red pill badge reading “REPORTER” sits above the name in 32px bold white type, with a muted bureau line below. The entire block slides in from the left using a tight damping: 20, stiffness: 180 spring over 12 frames, holds for 16 frames, then exits to the left from frame 28. A dark scrim with a 4px left red border keeps the text legible over any talent background. Scene 2 (frames 40–80) switches to a minimal floating card anchored to the top-right. A dark rgba(0,0,0,0.72) scrim with a hairline top border fades in over 10 frames using a cubic ease-out, revealing “Photo” in 11px muted uppercase, the copyright credit in 18px bold white, and a correspondent subtitle below. A narrow gold accent bar fades in beneath the card. The entire overlay fades back out by frame 80. Scene 3 (frames 80–120) fills the full-width bottom band. The band slides up from below via a damping: 24, stiffness: 140 spring, and a red progress bar scales across the top border simultaneously. Three production credits — PRODUCED BY, DIRECTED BY, and EDITED BY — each reveal character-by-character with staggered start frames (10, 28, and 46 in local time) at 1.0 frames per character. Red dot dividers pop in between entries. The NNX Digital network logo springs in from the far right at local frame 14.

A subtle scan-line overlay scrolls at 2px per frame for broadcast texture, and a muted monospace timecode in the top-left corner reinforces the on-air aesthetic without distracting from the credit content.

Composition specs

PropertyValue
Resolution1280 × 720
FPS30
Duration4.0 s (120 frames)

Timeline

TimeFramesAction
0:00 – 0:00.40 – 12Reporter byline springs in from left (damping 20, stiffness 180)
0:00.4 – 0:00.912 – 28Byline holds; red pill badge + name + bureau text fully visible
0:00.9 – 1:0328 – 40Byline slides out to the left; scene transitions to photographer
1:03 – 1:04.340 – 50Photographer card fades in top-right with float-down offset
1:04.3 – 2:02.350 – 68Photographer overlay holds; gold accent bar fully visible
2:02.3 – 2:02.768 – 80Photographer card fades out; production band prepares to slide up
2:02.7 – 2:03.680 – 98Full-width production band slides up; red border line scales across
2:03.6 – 3:0498 – 112Credits reveal letter-by-letter; network logo springs in right
3:04 – 4:00112 – 120Band holds full; all credits visible; fades out cleanly at 120

Customization

  • CREDITS — array of three credit objects. Object at index 0 drives Scene 1 (reporter byline: role, name, detail). Index 1 drives Scene 2 (photographer: name, detail). Index 2 drives Scene 3 (production strip: producers array of { label, value } pairs).
  • NETWORK_NAME — short call letters rendered inside the red circle logo (default: NNX).
  • NETWORK_TAGLINE — longer name shown beside the logo in Scene 3 (default: NNX Digital).
  • ACCENT_RED — primary red used for the pill badge, border bar, dot dividers, and logo circle (default: #e8001e).
  • ACCENT_GOLD — narrow accent bar color beneath the Scene 2 photographer card (default: #f5c842).
  • BG — main background fill (default: #0d1117).
  • SCRIM_DARK — semi-transparent card background behind bylines (default: rgba(0,0,0,0.72)).
  • BAND_BG — production strip background (default: rgba(10,12,20,0.96)).
  • FONT — font stack applied to all text layers (default: Inter, system-ui, -apple-system, sans-serif).