Stage
The shared backdrop: hairline grid, soft glow, and vignette.
Customize
import {Scene, Video} from "odori";
import {Stage} from "../components/stage/stage";
<Video>
<Scene id="stage" duration="4s">
<Stage
grid={true}
glow={true}
/>
</Scene>
</Video>Installation
pnpm odori add component stagenpx odori add component stagebunx odori add component stageEdit the source in videos/components/stage/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/stage/stage.tsx
import type {CSSProperties, ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type StageProps = {
children?: ReactNode;
/** A hairline grid keeps large empty frames from feeling flat. */
grid?: boolean;
/** A slow radial glow behind the content. */
glow?: boolean;
align?: "center" | "start";
style?: CSSProperties;
};
/**
* The shared backdrop. Every scene sits on the same surface so cuts feel like
* one film rather than a stack of unrelated slides.
*/
/** Relative luminance, so the backdrop adapts to a light or dark brand. */
const isLight = (hex: string) => {
const value = hex.replace("#", "");
const full = value.length === 3 ? value.split("").map((part) => part + part).join("") : value;
const [red, green, blue] = [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16) / 255);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue > 0.5;
};
export const Stage = ({children, grid = true, glow = true, align = "center", style}: StageProps) => {
const frame = useFrame();
const brand = useBrand();
const {width, height} = useVideo();
const scale = useDesignScale();
const drift = interpolate(frame, [0, 240], [0, 1], {extrapolateRight: "extend"});
const cell = 80 * scale;
const light = isLight(brand.colors.background);
const glowColor = light ? "rgba(0,0,0,0.05)" : "rgba(255,255,255,0.07)";
const vignette = light ? "rgba(0,0,0,0.06)" : "rgba(0,0,0,0.55)";
return (
<Fill style={{background: brand.colors.background, ...style}}>
{grid ? (
<Fill
style={{
backgroundImage: `linear-gradient(to right, ${brand.colors.border} 1px, transparent 1px), linear-gradient(to bottom, ${brand.colors.border} 1px, transparent 1px)`,
backgroundSize: `${cell}px ${cell}px`,
maskImage: `radial-gradient(ellipse ${width * 0.55}px ${height * 0.5}px at 50% 45%, rgba(0,0,0,0.65), transparent 78%)`,
WebkitMaskImage: `radial-gradient(ellipse ${width * 0.55}px ${height * 0.5}px at 50% 45%, rgba(0,0,0,0.65), transparent 78%)`,
opacity: interpolate(frame, [0, 24], [0, 0.7], {easing: Easing.standard}),
transform: `translateY(${drift * -14 * scale}px)`,
}}
/>
) : null}
{glow ? (
<Fill
style={{
backgroundImage: `radial-gradient(ellipse ${width * 0.45}px ${height * 0.42}px at 50% ${
38 + drift * 3
}%, ${glowColor}, transparent 70%)`,
}}
/>
) : null}
<Fill
style={{
alignItems: "center",
justifyContent: align === "center" ? "center" : "flex-start",
}}
>
{children}
</Fill>
<Fill
style={{
boxShadow: `inset 0 0 ${240 * scale}px ${vignette}`,
pointerEvents: "none",
}}
/>
</Fill>
);
};
videos/components/stage/stage.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {Stage} from "./stage";
export default defineComponentPreview({
title: "Stage",
category: "Foundation",
description: "The shared backdrop: hairline grid, soft glow, and vignette.",
component: Stage,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
grid: {type: "boolean", defaultValue: true},
glow: {type: "boolean", defaultValue: true},
},
examples: [
{name: "Default", props: {}},
{name: "Flat", props: {grid: false, glow: false}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Stage} from "../components/stage/stage";
<Video>
<Scene id="stage" duration="4s">
<Stage
grid={true}
glow={true}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Foundation
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 24 frames · 0.8s
- Entrance
- 24 frames
- Exit
- 0 frames
- Reduced motion
- static grid without drift
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | — |
gridA hairline grid keeps large empty frames from feeling flat. | boolean | true | — |
glowA slow radial glow behind the content. | boolean | true | — |
align | "center" | "start" | "center" | — |
style | CSSProperties | — | — |
Related components
All componentsAbsolute stageFull-frame surface with stable clipping and positioning.Safe areaFormat-aware title and action safety boundaries.Split layoutResponsive two-up composition for proof and explanation.AccordionSections opening one at a time, the outgoing one closing as the next grows.Area chartFilled trend reveal for volume and cumulative change.Aurora veilFlowing curtains of colored light.