Before and after
Direct visual comparison with a controlled divider.
Customize
import {Scene, Video} from "odori";
import {BeforeAfter} from "../components/before-after/before-after";
<Video>
<Scene id="before-after" duration="4s">
<BeforeAfter
beforeLabel="Before"
afterLabel="After"
settleAt={0.5}
/>
</Scene>
</Video>Installation
pnpm odori add component before-afternpx odori add component before-afterbunx odori add component before-afterEdit the source in videos/components/before-after/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/before-after/before-after.tsx
import type {ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type BeforeAfterProps = {
before: ReactNode;
after: ReactNode;
beforeLabel?: string;
afterLabel?: string;
/** Where the divider ends up, 0 to 1. */
settleAt?: number;
};
/**
* One frame, two states, a divider that travels across it. Both sides are
* always rendered at full size, so nothing reflows as the divider moves.
*/
export const BeforeAfter = ({before, after, beforeLabel = "Before", afterLabel = "After", settleAt = 0.5}: BeforeAfterProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const position = interpolate(frame, [12, 60], [1, settleAt], {
easing: Easing.standard,
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const label = (text: string, side: "left" | "right") => (
<span
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 999,
bottom: 44 * scale,
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
left: side === "left" ? 44 * scale : undefined,
padding: `${10 * scale}px ${20 * scale}px`,
position: "absolute",
right: side === "right" ? 44 * scale : undefined,
}}
>
{text}
</span>
);
return (
<Fill>
<Fill>{after}</Fill>
<Fill style={{clipPath: `inset(0 ${(1 - position) * 100}% 0 0)`}}>{before}</Fill>
<Fill style={{pointerEvents: "none"}}>
<div
style={{
background: brand.colors.accent,
bottom: 0,
left: `${position * 100}%`,
position: "absolute",
top: 0,
width: 3 * scale,
}}
/>
{label(beforeLabel, "left")}
{label(afterLabel, "right")}
</Fill>
</Fill>
);
};
videos/components/before-after/before-after.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {BeforeAfter} from "./before-after";
export default defineComponentPreview({
title: "Before and after",
category: "Narrative",
description: "Direct visual comparison with a controlled divider.",
component: BeforeAfter,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
beforeLabel: {type: "text", defaultValue: "Before"},
afterLabel: {type: "text", defaultValue: "After"},
settleAt: {type: "number", defaultValue: 0.5, min: 0.1, max: 0.9, step: 0.05},
},
examples: [
{
name: "Timeline",
props: {
before: "Hand-numbered frames",
after: "Ordered scenes",
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {BeforeAfter} from "../components/before-after/before-after";
<Video>
<Scene id="before-after" duration="4s">
<BeforeAfter
beforeLabel="Before"
afterLabel="After"
settleAt={0.5}
/>
</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
- 120 frames · 4s
- Minimum duration
- 72 frames · 2.4s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- the divider starts at its resting position
- Requires
- sans font, mono font
- Content limits
- beforeLabel ≤ 18, afterLabel ≤ 18
Props
| Prop | Type | Default | Required |
|---|---|---|---|
before | ReactNode | — | Yes |
after | ReactNode | — | Yes |
beforeLabel | string | "Before" | — |
afterLabel | string | "After" | — |
settleAtWhere the divider ends up, 0 to 1. | number | 0.5 | — |
Related components
All componentsComparisonTwo options contrasted through matched structure.Connection storyA left-to-right pipeline of connected stages.End cardThe closing frame: title, supporting line, and a call to action.Feature stackA sequence of features in one card.State transitionBefore, processing, and resolved product states.TimelineMilestones revealed along a timeline.