brand-provider
Typed color, type, spacing, radius, and motion tokens.
import {Scene, Video} from "odori";
import {BrandProvider} from "../components/brand-provider/brand-provider";
<Video>
<Scene id="brand-provider" duration="4s">
<BrandProvider
title="Brand tokens"
/>
</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/brand-providernpx odori add @odori/brand-providerbunx odori add @odori/brand-providerSource is copied into videos/components/brand-provider/ 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/brand-provider/brand-provider.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type BrandProviderProps = {
/** Which token groups to show. */
show?: Array<"colors" | "type" | "motion">;
title?: string;
};
/**
* Reads the resolved brand and renders it. Every token on screen is the value
* the surrounding video actually uses, so this doubles as a brand sheet and as
* a check that a layout inherited what you expected.
*/
export const BrandProvider = ({show = ["colors", "type", "motion"], title = "Brand tokens"}: BrandProviderProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const swatches = Object.entries(brand.colors);
const row = (index: number) => {
const delay = 8 + index * 4;
const progress = interpolate(frame, [delay, delay + 20], [0, 1], {easing: Easing.standard});
return {opacity: progress, transform: `translateY(${(1 - progress) * 18 * scale}px)`};
};
return (
<Fill
style={{
gap: 40 * scale,
justifyContent: "center",
padding: `${110 * scale}px ${130 * scale}px`,
}}
>
<div style={{fontSize: 62 * scale, fontWeight: 570, letterSpacing: "-0.04em", ...row(0)}}>{title}</div>
{show.includes("colors") ? (
<div style={{display: "flex", flexWrap: "wrap", gap: 18 * scale}}>
{swatches.map(([name, value], index) => (
<div key={name} style={{alignItems: "center", display: "flex", gap: 12 * scale, ...row(index + 1)}}>
<span
style={{
background: value,
border: `1px solid ${brand.colors.border}`,
borderRadius: 12 * scale,
height: 56 * scale,
width: 56 * scale,
}}
/>
<span style={{fontFamily: brand.typography.mono, fontSize: 24 * scale}}>
{name}
<span style={{color: brand.colors.muted, display: "block"}}>{value}</span>
</span>
</div>
))}
</div>
) : null}
{show.includes("type") ? (
<div style={{display: "grid", gap: 8 * scale, ...row(swatches.length + 1)}}>
<span style={{fontFamily: brand.typography.sans, fontSize: 44 * scale}}>Sans, for statements</span>
<span style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 30 * scale}}>
Mono, for evidence
</span>
</div>
) : null}
{show.includes("motion") ? (
<div
style={{
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 26 * scale,
...row(swatches.length + 2),
}}
>
standard [{brand.motion.standard.join(", ")}] · stagger {brand.motion.staggerFrames}f
</div>
) : null}
</Fill>
);
};
videos/components/brand-provider/brand-provider.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {BrandProvider} from "./brand-provider";
export default defineComponentPreview({
title: "Brand provider",
category: "Foundation",
description: "Typed color, type, spacing, radius, and motion tokens.",
component: BrandProvider,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
title: {type: "text", defaultValue: "Brand tokens"},
},
examples: [
{name: "Everything", props: {}},
{name: "Colors only", props: {show: ["colors"], title: "Palette"}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {BrandProvider} from "../components/brand-provider/brand-provider";
<Video>
<Scene id="brand-provider" duration="4s">
<BrandProvider
title="Brand tokens"
/>
</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
- Foundation
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 60 frames · 2s
- Entrance
- 40 frames
- Exit
- 0 frames
- Reduced motion
- rows appear together
- Requires
- sans font, mono font
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
showWhich token groups to show. | Array<"colors" | "type" | "motion"> | ["colors" | — |
title | string | "Brand tokens" | — |