Media

Captions Overlay

A video captions/subtitle styling preview with adjustable font size, text color, and background box treatment.

Install

npx shadcn@latest add @paragon/captions-overlay

captions-overlay.tsx

"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

export type CaptionSize = "sm" | "md" | "lg" | "xl";
export type CaptionBackground = "solid" | "translucent" | "none";

export interface CaptionsOverlayProps extends React.ComponentProps<"div"> {
  /** The active caption line. */
  text?: string;
  /** Speaker label rendered before the line. */
  speaker?: string;
  /** Font size preset. */
  size?: CaptionSize;
  /** Caption text color (hex). */
  color?: string;
  /** Background box treatment behind the caption. */
  background?: CaptionBackground;
  /** Two hex colors for the video backdrop placeholder. */
  backdrop?: [string, string];
}

const SIZE_CLASS: Record<CaptionSize, string> = {
  sm: "text-xs",
  md: "text-sm",
  lg: "text-base",
  xl: "text-lg",
};

/**
 * A video captions/subtitle styling preview. Renders a placeholder video frame
 * with a single caption line, letting you audition font size, text color, and
 * background treatment (solid / translucent / none) — the same knobs a player's
 * accessibility settings expose. Static by design; no animation.
 */
export function CaptionsOverlay({
  text = "We shipped the entire design system in under ninety days.",
  speaker = "Narrator",
  size = "md",
  color = "#ffffff",
  background = "translucent",
  backdrop = ["#1e293b", "#0f172a"],
  className,
  ...props
}: CaptionsOverlayProps) {
  const bgStyle =
    background === "solid"
      ? "bg-black"
      : background === "translucent"
        ? "bg-black/60 backdrop-blur-sm"
        : "";
  const textShadow =
    background === "none"
      ? { textShadow: "0 1px 3px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.7)" }
      : undefined;

  return (
    <div
      data-slot="captions-overlay"
      className={cn(
        "relative aspect-video w-full max-w-xl overflow-hidden rounded-xl shadow-border",
        className,
      )}
      {...props}
    >
      <div
        aria-hidden
        className="absolute inset-0"
        style={{
          background: `linear-gradient(135deg, ${backdrop[0]}, ${backdrop[1]})`,
        }}
      />
      <div
        aria-hidden
        className="absolute inset-0 grid place-items-center text-white/15"
      >
        <svg viewBox="0 0 24 24" className="size-16" fill="currentColor">
          <path d="M8 5v14l11-7z" />
        </svg>
      </div>

      <div className="absolute inset-x-0 bottom-0 flex justify-center px-4 pb-[10%]">
        <p
          className={cn(
            "max-w-[90%] rounded-md px-2.5 py-1 text-center font-medium leading-snug text-pretty",
            SIZE_CLASS[size],
            bgStyle,
          )}
          style={{ color, ...textShadow }}
        >
          {speaker && (
            <span className="opacity-70">[{speaker}] </span>
          )}
          {text}
        </p>
      </div>
    </div>
  );
}