Libraries
Libraries
Story Scenes
Draws each of the storyboard's 15 scene types from catalog items; `storyboard` renders stories with it.
Install
Usage
<SceneView scene={{ type: "stat", label: "Weekly users", value: 48200, delta: 12 }} />Dependencies
@remotion/media, remotion
Source
registry/items/story-scenes.tsx
/**
* @title Story Scenes
* @category lib
* @description Draws each of the storyboard's 15 scene types from catalog items; `storyboard` renders stories with it.
* @tags storyboard, scenes
* @example
* <SceneView scene={{ type: "stat", label: "Weekly users", value: 48200, delta: 12 }} />
*/
import { Video } from "@remotion/media";
import type React from "react";
import { AbsoluteFill, Img, interpolate, useVideoConfig } from "remotion";
import { Animate } from "./animate";
import { Aurora } from "./aurora";
import { BarChart } from "./bar-chart";
import { Beams } from "./beams";
import { Bokeh } from "./bokeh";
import { BrandSolid } from "./brand-solid";
import { BrowserWindow } from "./browser-window";
import { CodeBlock } from "./code-block";
import { alpha, Center, CLAMP, useMotion, useTheme, useViewport } from "./core";
import { Donut } from "./donut";
import { Dots } from "./dots";
import { FeatureCard } from "./feature-card";
import { GradientMesh } from "./gradient-mesh";
import { Grain } from "./grain";
import { Grid } from "./grid";
import { LaptopFrame } from "./laptop-frame";
import { LineChart } from "./line-chart";
import { LowerThird } from "./lower-third";
import { PhoneFrame } from "./phone-frame";
import { PostCard } from "./post-card";
import { QuoteCard } from "./quote-card";
import { SplitScreen } from "./split-screen";
import { Spotlight } from "./spotlight";
import { Stagger } from "./stagger";
import { Starfield } from "./starfield";
import { StatCounter } from "./stat-counter";
import { CODE_CPS, isVideo, type Scene, type SceneOf } from "./story";
import { Terminal } from "./terminal";
import { TextReveal } from "./text-reveal";
/** Every background a scene can name, keyed by its registry item name. */
export const BACKGROUNDS: Record<string, React.ComponentType> = {
aurora: Aurora,
beams: Beams,
bokeh: Bokeh,
"brand-solid": BrandSolid,
dots: Dots,
"gradient-mesh": GradientMesh,
grain: Grain,
grid: Grid,
spotlight: Spotlight,
starfield: Starfield,
};
/** A named background, full-bleed behind a scene. */
export function Backdrop({ name }: { name?: string }) {
const Background = name ? BACKGROUNDS[name] : undefined;
return Background ? <Background /> : null;
}
/** Headline size in design units: smaller as the line gets longer. */
const headline = (text: string, size: number) =>
text.length > 60 ? size * 0.55 : text.length > 28 ? size * 0.75 : size;
const cover: React.CSSProperties = { width: "100%", height: "100%", objectFit: "cover" };
const avatarImage = (src?: string) => (src ? <Img src={src} style={cover} /> : undefined);
/** Stand-in for a missing screenshot: a few themed interface bars, so templates render with no assets. */
function ScreenPlaceholder() {
const theme = useTheme();
const { u } = useViewport();
const bar = (width: string, height: number, color: string) => (
<div style={{ width, height: u(height), borderRadius: u(10), background: color }} />
);
return (
<div
style={{
width: "100%",
height: "100%",
boxSizing: "border-box",
padding: u(32),
display: "flex",
flexDirection: "column",
gap: u(18),
background: theme.colors.surface,
}}
>
{bar("40%", 28, alpha(theme.colors.foreground, 0.5))}
{bar("100%", 160, alpha(theme.colors.accent, 0.35))}
{bar("85%", 18, alpha(theme.colors.foreground, 0.2))}
{bar("70%", 18, alpha(theme.colors.foreground, 0.2))}
{bar("90%", 18, alpha(theme.colors.foreground, 0.2))}
</div>
);
}
/** An image or a muted video filling its box, or the placeholder screen when there is no source. */
function Media({ src }: { src?: string }) {
if (!src) return <ScreenPlaceholder />;
return isVideo(src) ? (
<Video src={src} muted objectFit="cover" style={{ width: "100%", height: "100%" }} />
) : (
<Img src={src} style={cover} />
);
}
function TitleScene({ title, subtitle, kicker }: SceneOf<"title">) {
const theme = useTheme();
const { u } = useViewport();
return (
<Center style={{ gap: u(28) }}>
{kicker ? (
<TextReveal text={kicker} size={34} font="body" weight={600} color={theme.colors.accent} effect="fade" />
) : null}
<TextReveal text={title} size={headline(title, 132)} />
{subtitle ? (
<TextReveal
text={subtitle}
size={44}
font="body"
weight={400}
color={theme.colors.muted}
effect="blur"
delay={8}
/>
) : null}
</Center>
);
}
function TextScene({ text, accent }: SceneOf<"text">) {
return (
<Center>
<TextReveal text={text} size={headline(text, 110)} accentWords={accent} />
</Center>
);
}
function BulletsScene({ title, items }: SceneOf<"bullets">) {
const { u } = useViewport();
return (
<Center style={{ gap: u(48) }}>
{title ? <TextReveal text={title} size={72} /> : null}
<Stagger gap={24} wrap delay={title ? 10 : 0}>
{items.map((item, index) => (
<FeatureCard key={index} title={item.text} body={item.detail} badge={item.badge} />
))}
</Stagger>
</Center>
);
}
function ImageScene({ src, caption, zoom = true }: SceneOf<"image">) {
const m = useMotion({ exit: false });
const scale = zoom ? interpolate(m.frame, [0, m.durationInFrames], [1, 1.08], CLAMP) : 1;
return (
<AbsoluteFill>
<AbsoluteFill style={{ scale: String(scale) }}>
<Media src={src} />
</AbsoluteFill>
{caption ? <LowerThird name={caption} variant="card" /> : null}
</AbsoluteFill>
);
}
function VideoScene({ src, trimStart = 0, trimEnd, muted }: SceneOf<"video">) {
const { fps } = useVideoConfig();
return (
<AbsoluteFill>
<Video
src={src}
muted={muted}
objectFit="cover"
style={{ width: "100%", height: "100%" }}
trimBefore={trimStart > 0 ? Math.round(trimStart * fps) : undefined}
trimAfter={trimEnd === undefined ? undefined : Math.round(trimEnd * fps)}
/>
</AbsoluteFill>
);
}
function DeviceScene({ device, src, url }: SceneOf<"device">) {
const screen = <Media src={src} />;
return (
<Center>
{device === "phone" ? (
<PhoneFrame>{screen}</PhoneFrame>
) : device === "laptop" ? (
<LaptopFrame>{screen}</LaptopFrame>
) : (
<BrowserWindow url={url}>{screen}</BrowserWindow>
)}
</Center>
);
}
function CodeScene({ code, language, title, highlight }: SceneOf<"code">) {
return (
<Center>
<CodeBlock code={code} language={language} title={title} typing={CODE_CPS} highlightLines={highlight} />
</Center>
);
}
function TerminalScene({ lines, title }: SceneOf<"terminal">) {
return (
<Center>
<Terminal lines={lines} title={title} />
</Center>
);
}
function ChartScene({ kind, title, data }: SceneOf<"chart">) {
const { u } = useViewport();
return (
<Center style={{ gap: u(36) }}>
{title ? <TextReveal text={title} size={56} /> : null}
{kind === "bar" ? (
<BarChart data={data} delay={6} />
) : kind === "line" ? (
<LineChart
series={[{ label: title ?? "Value", points: data.map((d) => d.value) }]}
labels={data.map((d) => d.label)}
delay={6}
/>
) : (
<Donut data={data} delay={6} />
)}
</Center>
);
}
function StatScene({ label, value, prefix, suffix, delta, caption }: SceneOf<"stat">) {
return (
<Center>
<StatCounter
label={label}
to={value}
prefix={prefix}
suffix={suffix}
delta={delta}
caption={caption}
size={160}
/>
</Center>
);
}
function QuoteScene({ quote, name, role, avatar }: SceneOf<"quote">) {
return (
<Center>
<QuoteCard
quote={quote}
name={name}
role={role}
avatar={avatarImage(avatar)}
size={quote.length > 140 ? 36 : 46}
/>
</Center>
);
}
function PostScene({ name, handle, text, avatar, metrics }: SceneOf<"post">) {
return (
<Center>
<PostCard name={name} handle={handle} text={text} avatar={avatarImage(avatar)} metrics={metrics} />
</Center>
);
}
function CtaScene({ title, button, url }: SceneOf<"cta">) {
const theme = useTheme();
const { u } = useViewport();
return (
<Center style={{ gap: u(40) }}>
<TextReveal text={title} size={headline(title, 104)} />
{button ? (
<Animate effect="pop" delay={12}>
<div
style={{
padding: `${u(22)}px ${u(48)}px`,
borderRadius: u(999),
background: theme.colors.accent,
color: theme.colors.accentForeground,
fontFamily: theme.fonts.heading,
fontWeight: theme.headingWeight,
fontSize: u(40),
}}
>
{button}
</div>
</Animate>
) : null}
{url ? <TextReveal text={url} size={36} font="mono" color={theme.colors.muted} effect="fade" delay={20} /> : null}
</Center>
);
}
function LogoScene({ text, src }: SceneOf<"logo">) {
const { u } = useViewport();
return (
<Center style={{ gap: u(32) }}>
{src ? (
<Animate effect="pop">
<Img src={src} style={{ height: u(180), width: "auto" }} />
</Animate>
) : null}
<TextReveal text={text} size={src ? 64 : 120} effect={src ? "fade" : "scale"} delay={src ? 10 : 0} />
</Center>
);
}
function SplitScene({ left, right, labels }: SceneOf<"split">) {
return (
<SplitScreen labels={labels}>
<SceneView scene={left} />
<SceneView scene={right} />
</SplitScreen>
);
}
function SceneContent({ scene }: { scene: Scene }) {
switch (scene.type) {
case "title":
return <TitleScene {...scene} />;
case "text":
return <TextScene {...scene} />;
case "bullets":
return <BulletsScene {...scene} />;
case "image":
return <ImageScene {...scene} />;
case "video":
return <VideoScene {...scene} />;
case "device":
return <DeviceScene {...scene} />;
case "code":
return <CodeScene {...scene} />;
case "terminal":
return <TerminalScene {...scene} />;
case "chart":
return <ChartScene {...scene} />;
case "stat":
return <StatScene {...scene} />;
case "quote":
return <QuoteScene {...scene} />;
case "post":
return <PostScene {...scene} />;
case "cta":
return <CtaScene {...scene} />;
case "logo":
return <LogoScene {...scene} />;
case "split":
return <SplitScene {...scene} />;
}
}
/** Draws one built-in scene over its background. */
export function SceneView({ scene }: { scene: Scene }) {
return (
<>
<Backdrop name={scene.background} />
<SceneContent scene={scene} />
</>
);
}