R RenderComp
remotion broadcast lower-thirds news graphics

Remotion for Broadcast News Graphics: Lower Thirds, Tickers, and Live Overlays

Remotion for Broadcast News Graphics: Lower Thirds, Tickers, and Live Overlays

Broadcast news graphics have always sat at the intersection of design and engineering. The lower third that wipes in to identify a speaker, the election results bar that fills in real time, the breaking news banner that demands attention — every one of those elements is a timed animation driven by data. Remotion is, at its core, a React-based video rendering engine, which makes it exceptionally well-suited to this exact use case: data-driven, precisely timed, composable graphics.

This guide walks through the key broadcast graphic types, shows working Remotion code for each, and covers transparent background export for professional compositing workflows.

The Broadcast Motion Design Philosophy

Traditional broadcast graphics pipelines — Vizrt, Chyron, Ross Xpression — are powerful but expensive, proprietary, and require specialized operators. The trade-off that Remotion offers is different: you give up real-time GPU rendering in exchange for a workflow built entirely on open web standards (React, TypeScript, CSS) that any frontend developer can maintain and extend.

The result is not a live CG system replacement. It is a production-quality, scriptable, data-driven graphics renderer that outputs finished video files or PNG sequences at any resolution. For news programmes, documentaries, corporate news channels, streaming shows, and podcast video productions, that capability covers the vast majority of use cases.

Lower Thirds: Name and Title Wipe Animation

The lower third is the most iconic broadcast graphic. A standard implementation uses a two-phase animation: the bar wipes in horizontally, then the text fades up.

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

interface LowerThirdProps {
  name: string;
  title: string;
  accentColor?: string;
}

export const LowerThird: React.FC<LowerThirdProps> = ({
  name,
  title,
  accentColor = "#e63946",
}) => {
  const frame = useCurrentFrame();

  // Phase 1: bar wipes in (frames 0–18)
  const barWidth = spring({
    frame,
    fps: 30,
    config: { damping: 18, stiffness: 120, mass: 1 },
    durationInFrames: 18,
  });

  // Phase 2: text fades up (starts at frame 12, overlaps with bar)
  const textOpacity = interpolate(frame, [12, 28], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const textY = interpolate(frame, [12, 28], [12, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill>
      <div
        style={{
          position: "absolute",
          bottom: "12%",
          left: "5%",
          overflow: "hidden",
        }}
      >
        {/* Colour accent bar */}
        <div
          style={{
            width: interpolate(barWidth, [0, 1], [0, 420]),
            height: 4,
            backgroundColor: accentColor,
            marginBottom: 10,
          }}
        />

        {/* Name + title block */}
        <div
          style={{
            opacity: textOpacity,
            transform: `translateY(${textY}px)`,
            backgroundColor: "rgba(10,10,20,0.85)",
            padding: "12px 20px",
            borderLeft: `4px solid ${accentColor}`,
            backdropFilter: "blur(4px)",
          }}
        >
          <div
            style={{
              fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
              fontSize: 32,
              fontWeight: 700,
              color: "#ffffff",
              letterSpacing: "0.02em",
              lineHeight: 1.2,
            }}
          >
            {name}
          </div>
          <div
            style={{
              fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
              fontSize: 18,
              fontWeight: 400,
              color: accentColor,
              marginTop: 4,
              letterSpacing: "0.06em",
              textTransform: "uppercase",
            }}
          >
            {title}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

The spring() animation on bar width gives the wipe a physical deceleration that feels intentional rather than mechanical. The overlapping phase start (text starts at frame 12, bar takes until frame 18) creates a snappy double-beat rhythm that is characteristic of broadcast motion design.

Election Results: Animated Bar Chart

Data-driven bar charts are the centrepiece of election night coverage. Each bar should animate to its value, and percentages should count up simultaneously.

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

interface Candidate {
  name: string;
  votes: number;
  color: string;
}

interface ElectionResultsProps {
  candidates: Candidate[];
  totalVotes: number;
}

export const ElectionResults: React.FC<ElectionResultsProps> = ({
  candidates,
  totalVotes,
}) => {
  const frame = useCurrentFrame();

  return (
    <AbsoluteFill
      style={{
        backgroundColor: "#0a0a14",
        justifyContent: "center",
        alignItems: "center",
        padding: "0 120px",
      }}
    >
      <div style={{ width: "100%" }}>
        <div
          style={{
            fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
            fontSize: 24,
            color: "#aaaacc",
            textTransform: "uppercase",
            letterSpacing: "0.15em",
            marginBottom: 40,
          }}
        >
          ELECTION RESULTS
        </div>

        {candidates.map((candidate, i) => {
          const targetPct = (candidate.votes / totalVotes) * 100;
          const delay = i * 8; // stagger each bar by 8 frames

          const progress = spring({
            frame: Math.max(0, frame - delay),
            fps: 30,
            config: { damping: 16, stiffness: 80, mass: 1.2 },
          });

          const pct = interpolate(progress, [0, 1], [0, targetPct]);
          const displayPct = Math.round(pct);

          return (
            <div key={i} style={{ marginBottom: 36 }}>
              {/* Candidate name and live percentage */}
              <div
                style={{
                  display: "flex",
                  justifyContent: "space-between",
                  fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                  color: "#fff",
                  fontSize: 28,
                  fontWeight: 600,
                  marginBottom: 10,
                }}
              >
                <span>{candidate.name}</span>
                <span style={{ color: candidate.color }}>{displayPct}%</span>
              </div>

              {/* Track */}
              <div
                style={{
                  width: "100%",
                  height: 20,
                  backgroundColor: "rgba(255,255,255,0.08)",
                  borderRadius: 4,
                  overflow: "hidden",
                }}
              >
                {/* Fill bar */}
                <div
                  style={{
                    width: `${pct}%`,
                    height: "100%",
                    backgroundColor: candidate.color,
                    borderRadius: 4,
                    transition: "none",
                  }}
                />
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

Pass this component defaultProps in your composition:

defaultProps={{
  candidates: [
    { name: "Candidate A", votes: 52400, color: "#3a86ff" },
    { name: "Candidate B", votes: 41200, color: "#e63946" },
    { name: "Candidate C", votes: 11900, color: "#f4a261" },
  ],
  totalVotes: 105500,
}}

Breaking News Banner

The breaking news banner is the most urgent element in broadcast design. It demands attention with high contrast, bold typography, and a rapid animation entrance.

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

interface BreakingNewsProps {
  headline: string;
  subline?: string;
}

export const BreakingNewsBanner: React.FC<BreakingNewsProps> = ({
  headline,
  subline,
}) => {
  const frame = useCurrentFrame();

  const slideIn = spring({
    frame,
    fps: 30,
    config: { damping: 22, stiffness: 200, mass: 0.8 },
  });

  const translateY = interpolate(slideIn, [0, 1], [120, 0]);
  const opacity = interpolate(slideIn, [0, 1], [0, 1]);

  // Pulse animation for the "BREAKING" label
  const pulse = Math.sin(frame * 0.18) * 0.5 + 0.5; // 0–1 oscillation
  const labelOpacity = interpolate(pulse, [0, 1], [0.7, 1]);

  return (
    <AbsoluteFill>
      <div
        style={{
          position: "absolute",
          bottom: 0,
          left: 0,
          right: 0,
          transform: `translateY(${translateY}px)`,
          opacity,
        }}
      >
        {/* Red accent strip */}
        <div style={{ height: 6, backgroundColor: "#e63946" }} />

        {/* Main banner body */}
        <div
          style={{
            backgroundColor: "rgba(8,8,20,0.95)",
            padding: "20px 48px",
            display: "flex",
            alignItems: "center",
            gap: 24,
          }}
        >
          {/* BREAKING label */}
          <div
            style={{
              backgroundColor: "#e63946",
              padding: "6px 14px",
              opacity: labelOpacity,
              flexShrink: 0,
            }}
          >
            <span
              style={{
                fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                fontSize: 18,
                fontWeight: 900,
                color: "#fff",
                letterSpacing: "0.12em",
                textTransform: "uppercase",
              }}
            >
              BREAKING
            </span>
          </div>

          {/* Headline */}
          <div>
            <div
              style={{
                fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                fontSize: 34,
                fontWeight: 700,
                color: "#ffffff",
                lineHeight: 1.15,
              }}
            >
              {headline}
            </div>
            {subline && (
              <div
                style={{
                  fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                  fontSize: 20,
                  color: "#aaaacc",
                  marginTop: 4,
                }}
              >
                {subline}
              </div>
            )}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

Sports Score Overlay

Score bugs are permanently-present elements that must be legible across a variety of background content. They live in the corner and update as scores change.

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

interface ScoreOverlayProps {
  homeTeam: string;
  awayTeam: string;
  homeScore: number;
  awayScore: number;
  period: string;
  clock: string;
}

export const ScoreOverlay: React.FC<ScoreOverlayProps> = ({
  homeTeam,
  awayTeam,
  homeScore,
  awayScore,
  period,
  clock,
}) => {
  const { width, height } = useVideoConfig();
  const frame = useCurrentFrame();

  const opacity = interpolate(frame, [0, 10], [0, 1], {
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill>
      <div
        style={{
          position: "absolute",
          top: height * 0.04,
          left: width * 0.03,
          opacity,
        }}
      >
        <div
          style={{
            backgroundColor: "rgba(0,0,0,0.82)",
            borderRadius: 6,
            overflow: "hidden",
            minWidth: 220,
          }}
        >
          {/* Period / clock row */}
          <div
            style={{
              backgroundColor: "#e63946",
              padding: "4px 12px",
              textAlign: "center",
              fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
              fontSize: 13,
              fontWeight: 700,
              color: "#fff",
              letterSpacing: "0.1em",
            }}
          >
            {period} · {clock}
          </div>

          {/* Teams + scores */}
          {[{ team: homeTeam, score: homeScore }, { team: awayTeam, score: awayScore }].map(
            (row, i) => (
              <div
                key={i}
                style={{
                  display: "flex",
                  justifyContent: "space-between",
                  alignItems: "center",
                  padding: "10px 14px",
                  borderTop: i > 0 ? "1px solid rgba(255,255,255,0.1)" : "none",
                }}
              >
                <span
                  style={{
                    fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                    fontSize: 20,
                    fontWeight: 600,
                    color: "#fff",
                  }}
                >
                  {row.team}
                </span>
                <span
                  style={{
                    fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
                    fontSize: 24,
                    fontWeight: 700,
                    color: "#fff",
                    minWidth: 36,
                    textAlign: "right",
                  }}
                >
                  {row.score}
                </span>
              </div>
            )
          )}
        </div>
      </div>
    </AbsoluteFill>
  );
};

Horizontal News Ticker

The ticker is a horizontally scrolling stream of text at the bottom of the screen. In Remotion, you drive scroll position with useCurrentFrame().

import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";

interface TickerProps {
  items: string[];
  speed?: number; // pixels per frame
}

export const NewsTicker: React.FC<TickerProps> = ({ items, speed = 3 }) => {
  const frame = useCurrentFrame();
  const { width, height } = useVideoConfig();

  const tickerText = items.join("  ·  ") + "  ·  " + items.join("  ·  ");
  const offset = -(frame * speed);

  return (
    <AbsoluteFill>
      <div
        style={{
          position: "absolute",
          bottom: 0,
          left: 0,
          right: 0,
          backgroundColor: "#e63946",
          height: height * 0.055,
          overflow: "hidden",
          display: "flex",
          alignItems: "center",
        }}
      >
        {/* Label */}
        <div
          style={{
            backgroundColor: "#0a0a14",
            height: "100%",
            padding: "0 20px",
            display: "flex",
            alignItems: "center",
            flexShrink: 0,
            zIndex: 2,
          }}
        >
          <span
            style={{
              fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
              fontSize: height * 0.025,
              fontWeight: 700,
              color: "#e63946",
              letterSpacing: "0.1em",
              textTransform: "uppercase",
            }}
          >
            LIVE
          </span>
        </div>

        {/* Scrolling text */}
        <div
          style={{
            transform: `translateX(${offset}px)`,
            whiteSpace: "nowrap",
            fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
            fontSize: height * 0.025,
            color: "#fff",
            fontWeight: 500,
            paddingLeft: 20,
          }}
        >
          {tickerText}
        </div>
      </div>
    </AbsoluteFill>
  );
};

For a seamless loop, set the composition durationInFrames so the ticker completes at least one full text cycle: durationInFrames = Math.ceil(textWidth / speed).

Transparent Background Export (PNG Sequence)

The most powerful broadcast workflow uses Remotion’s transparent PNG sequence export. This lets you composite Remotion graphics over live video in DaVinci Resolve, Premiere Pro, After Effects, or any NLE that supports alpha channels.

Render a transparent composition with:

# PNG sequence with alpha channel
npx remotion render LowerThird --codec=png --image-format=png ./output/frames/

# ProRes with alpha (for NLE import)
npx remotion render LowerThird --codec=prores --prores-profile=4444 ./output/lower-third.mov

In your composition, ensure the root <AbsoluteFill> has no backgroundColor set, or set it to "transparent". Remotion preserves the alpha channel through the render pipeline when using the png or prores codecs.

// For transparent export — no background colour
export const LowerThirdTransparent: React.FC<LowerThirdProps> = (props) => (
  <AbsoluteFill>
    {/* Content only — no background fill */}
    <LowerThirdContent {...props} />
  </AbsoluteFill>
);

ProRes 4444 is the industry standard for broadcast graphics delivery. It supports full 12-bit colour depth and a full alpha channel, and is natively supported by every major NLE. Render at your edit resolution (1920×1080 or 4K) to avoid quality loss.

Data-Driven Graphics from JSON

The real power of Remotion for broadcast is data-binding. Instead of manually animating every lower third, you feed a JSON file and generate the full graphics package automatically.

// Root.tsx — one composition per data entry
import { Composition } from "remotion";
import guestsData from "./data/guests.json";
import { LowerThird } from "./LowerThird";

export const RemotionRoot: React.FC = () => (
  <>
    {guestsData.map((guest) => (
      <Composition
        key={guest.id}
        id={`LowerThird-${guest.id}`}
        component={LowerThird}
        width={1920}
        height={1080}
        fps={30}
        durationInFrames={150}
        defaultProps={{ name: guest.name, title: guest.title }}
      />
    ))}
  </>
);

Batch render all compositions:

npx remotion render --bundle-cache=true

Or use the Remotion Lambda / Remotion Cloud for parallel rendering when you have 50+ lower thirds to produce.

Production Workflow Recommendations

Separation of concerns: Keep your data in JSON files separate from component code. This lets producers update content without touching the codebase.

Version your data: Use data-v001.json, data-v002.json naming and keep a latest symlink. This mirrors the broadcast tradition of “show packages” that correspond to air dates.

Test at delivery resolution: Always preview at the exact export resolution. Text at 28px looks crisp at 1080p and illegible at 720p. Use useVideoConfig() to scale fonts proportionally.

Output format by NLE:

  • DaVinci Resolve: ProRes 4444 .mov
  • Adobe Premiere: ProRes 4444 or PNG sequence
  • Final Cut Pro: ProRes 4444 .mov
  • Online delivery only: H.264 with matte approach (render twice: once with graphics, once black background)

Pre-Built Broadcast Templates from RenderComp

Building a full broadcast graphics package from scratch is a multi-day project. RenderComp at rendercomp.com offers a suite of professional Remotion broadcast templates — lower thirds packages, election graphics sets, sports score overlays, and ticker systems — all pre-built for transparent export and data-driven operation. Customise colours, fonts, and layouts to match your brand, then ship in hours instead of weeks.


Frequently Asked Questions

Q1: Can Remotion produce truly transparent video for broadcast compositing? Yes. Render with --codec=prores --prores-profile=4444 to get a ProRes 4444 .mov file with a full alpha channel. Alternatively, use --codec=png for a PNG image sequence. Both formats are accepted by DaVinci Resolve, Premiere Pro, and Final Cut Pro. Ensure your composition root has no opaque background fill.

Q2: How do I update lower thirds data without re-coding? Store names and titles in a JSON file imported as defaultProps. At render time, pass --props='{"name":"Alice Smith","title":"Chief Analyst"}' on the command line to override defaults. This lets a producer update content from a spreadsheet export without touching TypeScript.

Q3: Can I run Remotion in a broadcast automation pipeline? Yes. Remotion’s CLI is fully scriptable and integrates with CI/CD tools, cron jobs, and node-based automation. Remotion Lambda enables cloud-based parallel rendering, which is ideal for same-day deadline workflows like daily news packages.

Q4: What frame rate should I use for broadcast graphics? 25 fps for PAL/European broadcast markets, 29.97 (use 30 in Remotion and adjust in your NLE) for NTSC/US markets, 50 or 60 fps for sports highlights. Set fps in your <Composition> to match your edit timeline.

Q5: How do I create a ticker that loops seamlessly? Double the ticker text string (text + separator + text repeated) and set the composition duration to exactly one full scroll of the first half. Because the second half is identical to the first, the loop point is invisible. Alternatively, use a CSS @keyframes animation on a <style> tag — but note this will not be frame-deterministic across different render engines.

Q6: Is Remotion suitable for live broadcast or is it always pre-rendered? Remotion renders offline (pre-rendered). It is not a real-time rendering engine. For live broadcast integration, render graphics packages ahead of time (using automation to pull live data shortly before air) and play them as video files through your broadcast switcher or NLE. For truly live, frame-accurate CG, purpose-built broadcast systems are still necessary.

Q7: How do I match the font precisely to our on-air brand font? If your brand uses a proprietary font, install it as a system font on the render machine. Remotion uses the system font stack at render time. On Linux CI/CD servers, you may need to install fonts via apt-get or include them as .woff2 files loaded via the @remotion/google-fonts helper — though for broadcast, it is cleaner to install fonts system-wide.

Now available

Get 1,000+ Remotion Templates

Pay once — no subscription. Lifetime updates. TypeScript-first.

View pricing →