A video captions/subtitle styling preview with adjustable font size, text color, and background box treatment.
npx shadcn@latest add @paragon/captions-overlay"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>
);
}