browser-demo
Browser chrome for deterministic product interface frames.
import {Scene, Video} from "odori";
import {BrowserDemo} from "../components/browser-demo/browser-demo";
<Video>
<Scene id="browser-demo" duration="8s">
<BrowserDemo
url="odori.dev/docs"
/>
</Scene>
</Video>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/browser-demonpx odori add @odori/browser-demobunx odori add @odori/browser-demoSource is copied into videos/components/browser-demo/ and belongs to your repository. odori diff compares it with upstream later; odori update applies the change you accept. It has no registry dependencies.
Copy and paste the following code into your project.
videos/components/browser-demo/browser-demo.tsx
import type {ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type BrowserDemoProps = {
url: string;
children?: ReactNode;
/** Frame-driven cursor path in canvas coordinates. */
cursor?: Array<{frame: number; x: number; y: number}>;
};
/**
* A browser chrome frame for product UI. Application components can be passed
* as children when they render deterministically from frozen props.
*/
export const BrowserDemo = ({url, children, cursor = []}: BrowserDemoProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const appear = interpolate(frame, [0, 18], [0, 1], {easing: Easing.standard});
const position =
cursor.length >= 2
? {
x: interpolate(frame, cursor.map((point) => point.frame), cursor.map((point) => point.x)),
y: interpolate(frame, cursor.map((point) => point.frame), cursor.map((point) => point.y)),
}
: null;
return (
<Fill style={{alignItems: "center", justifyContent: "center", padding: 100 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 20 * scale,
boxShadow: `0 ${40 * scale}px ${80 * scale}px rgba(0,0,0,0.45)`,
height: "100%",
maxHeight: 860 * scale,
maxWidth: 1500 * scale,
opacity: appear,
overflow: "hidden",
position: "relative",
transform: `translateY(${(1 - appear) * 30 * scale}px) scale(${0.98 + appear * 0.02})`,
width: "100%",
}}
>
<div
style={{
alignItems: "center",
borderBottom: `1px solid ${brand.colors.border}`,
background: "rgba(255,255,255,0.02)",
display: "flex",
gap: 9 * scale,
padding: `${16 * scale}px ${22 * scale}px`,
}}
>
{[0, 1, 2].map((index) => (
<span key={index} style={{background: "#2a2a2a", borderRadius: 999, height: 12 * scale, width: 12 * scale}} />
))}
<div
style={{
background: brand.colors.background,
border: `1px solid ${brand.colors.border}`,
borderRadius: 999,
color: brand.colors.muted,
flex: 1,
fontFamily: brand.typography.mono,
fontSize: 20 * scale,
marginLeft: 12 * scale,
padding: `${8 * scale}px ${18 * scale}px`,
textAlign: "center",
}}
>
{url}
</div>
</div>
<div style={{flex: 1, position: "relative"}}>{children}</div>
{position ? (
<div
style={{
borderBottom: `${14 * scale}px solid ${brand.colors.foreground}`,
borderLeft: `${9 * scale}px solid transparent`,
borderRight: `${9 * scale}px solid transparent`,
height: 0,
left: position.x * scale,
position: "absolute",
top: position.y * scale,
transform: "rotate(-30deg)",
width: 0,
}}
/>
) : null}
</div>
</Fill>
);
};
videos/components/browser-demo/browser-demo.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {BrowserDemo} from "./browser-demo";
export default defineComponentPreview({
title: "Browser demo",
category: "Product UI",
description: "Browser chrome for deterministic product interface frames.",
component: BrowserDemo,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {url: {type: "text", defaultValue: "odori.dev/docs"}},
examples: [
{name: "Empty frame", props: {url: "odori.dev/docs"}},
{
name: "With cursor",
props: {
url: "odori.dev/components",
cursor: [
{frame: 0, x: 200, y: 200},
{frame: 60, x: 900, y: 520},
],
},
},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {BrowserDemo} from "../components/browser-demo/browser-demo";
<Video>
<Scene id="browser-demo" duration="8s">
<BrowserDemo
url="odori.dev/docs"
/>
</Scene>
</Video>Nothing else to install.
The component imports only from odori, so there are no dependencies to add and no import paths to rewrite.
Timing contract
Every registry component declares how it behaves in time.odori test fails a cut that violates these.
- Family
- Product UI
- Aspect ratios
- 16:9
- Recommended duration
- 240 frames · 8s
- Minimum duration
- 90 frames · 3s
- Entrance
- 18 frames
- Exit
- 12 frames
- Reduced motion
- no cursor motion
- Requires
- mono font, sans font
- Content limits
- url ≤ 48
Props
| Prop | Type | Default | Required |
|---|---|---|---|
url | string | — | Yes |
children | ReactNode | — | — |
cursorFrame-driven cursor path in canvas coordinates. | Array<{frame: number; x: number; y: number}> | [] | — |