Browser demo
Browser chrome for deterministic product interface frames.
Customize
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>Installation
pnpm odori add component browser-demonpx odori add component browser-demobunx odori add component browser-demoEdit the source in videos/components/browser-demo/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files 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 ${brand.colors.shadow}`,
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: brand.colors.surface, 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: "Interface/Chrome",
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},
],
},
},
],
});
Add it to 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>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Interface
- 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}> | [] | — |
Related components
All componentsAccordionSections opening one at a time, the outgoing one closing as the next grows.Card deckStacked cards that cycle from front to back.ComboboxA search field that filters a dropdown list.Command menuKeyboard-first action search and selection flow.Context menuThe menu that appears where you clicked, anchored to its own corner.Control stageAn enlarged control with an animated press.