End card
The closing frame: title, supporting line, and a call to action.
Customize
import {Scene, Video} from "odori";
import {EndCard} from "../components/end-card/end-card";
<Video>
<Scene id="end-card" duration="3s">
<EndCard
title="Author. Preview. Ship."
detail="An independent React video framework."
callToAction="npm i odori"
/>
</Scene>
</Video>Installation
pnpm odori add component end-cardnpx odori add component end-cardbunx odori add component end-cardEdit the source in videos/components/end-card/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/end-card/end-card.tsx
import {Fill, Easing, interpolate, spring, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type EndCardProps = {
title: string;
detail?: string;
callToAction?: string;
};
/** The closing lockup: the statement, and one command to run. */
export const EndCard = ({title, detail, callToAction}: EndCardProps) => {
const frame = useFrame();
const brand = useBrand();
const {fps} = useVideo();
const scale = useDesignScale();
const rise = spring({frame, fps, from: 0.96, to: 1, damping: 20});
const line = interpolate(frame, [8, 34], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", gap: 30 * scale, justifyContent: "center", padding: 110 * scale}}>
<div
style={{
fontSize: 108 * scale,
fontWeight: 560,
letterSpacing: "-0.05em",
lineHeight: 1.02,
maxWidth: 1400 * scale,
opacity: interpolate(frame, [0, 18], [0, 1], {easing: Easing.standard}),
textAlign: "center",
transform: `scale(${rise})`,
whiteSpace: "pre-line",
}}
>
{title}
</div>
<div
style={{
background: `linear-gradient(90deg, transparent, ${brand.colors.border}, transparent)`,
height: 1,
opacity: line,
width: `${line * 46}%`,
}}
/>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
letterSpacing: "-0.015em",
opacity: interpolate(frame, [14, 30], [0, 1], {easing: Easing.standard}),
textAlign: "center",
}}
>
{detail}
</div>
) : null}
{callToAction ? (
<div
style={{
alignItems: "center",
background: "rgba(255,255,255,0.04)",
border: `1px solid ${brand.colors.border}`,
borderRadius: 10 * scale,
color: brand.colors.foreground,
display: "flex",
fontFamily: brand.typography.mono,
fontSize: 28 * scale,
gap: 14 * scale,
marginTop: 8 * scale,
opacity: interpolate(frame, [22, 38], [0, 1], {easing: Easing.standard}),
padding: `${16 * scale}px ${28 * scale}px`,
transform: `translateY(${interpolate(frame, [22, 38], [12 * scale, 0], {easing: Easing.standard})}px)`,
}}
>
<span style={{color: brand.colors.muted}}>{"$"}</span>
{callToAction}
</div>
) : null}
</Fill>
);
};
videos/components/end-card/end-card.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {EndCard} from "./end-card";
export default defineComponentPreview({
title: "End card",
category: "Narrative",
description: "The closing frame: title, supporting line, and a call to action.",
component: EndCard,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
title: {type: "text", defaultValue: "Author. Preview. Ship.", maxLength: 48},
detail: {type: "text", defaultValue: "An independent React video framework."},
callToAction: {type: "text", defaultValue: "npm i odori"},
},
examples: [
{name: "Default", props: {title: "Author. Preview. Ship.", callToAction: "npm i odori"}},
{name: "Title only", props: {title: "Available today.", detail: undefined, callToAction: undefined}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {EndCard} from "../components/end-card/end-card";
<Video>
<Scene id="end-card" duration="3s">
<EndCard
title="Author. Preview. Ship."
detail="An independent React video framework."
callToAction="npm i odori"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Narrative
- Recommended duration
- 90 frames · 3s
- Minimum duration
- 45 frames · 1.5s
- Entrance
- 24 frames
- Exit
- 12 frames
- Reduced motion
- fades without scaling
- Requires
- sans font, mono font
- Content limits
- title ≤ 48, callToAction ≤ 28
Props
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
detail | string | — | — |
callToAction | string | — | — |
Related components
All componentsBefore and afterDirect visual comparison with a controlled divider.ComparisonTwo options contrasted through matched structure.Connection storyA left-to-right pipeline of connected stages.Feature stackA sequence of features in one card.State transitionBefore, processing, and resolved product states.TimelineMilestones revealed along a timeline.