safe-area
Format-aware title and action safety boundaries.
import {Scene, Video} from "odori";
import {SafeArea} from "../components/safe-area/safe-area";
<Video>
<Scene id="safe-area" duration="3s">
<SafeArea
guides={true}
inset={0}
label="safe area"
/>
</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/safe-areanpx odori add @odori/safe-areabunx odori add @odori/safe-areaSource is copied into videos/components/safe-area/ 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/safe-area/safe-area.tsx
import type {ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale, useLayout} from "odori";
export type SafeAreaGuidesProps = {
children?: ReactNode;
/** Draw the title and action boundaries. */
guides?: boolean;
/** Extra inset beyond the layout policy, in design pixels. */
inset?: number;
label?: string;
};
/**
* Keeps content inside the boundaries the layout declares, and can draw them.
* The numbers come from the resolved layout, so a 9:16 cut gets the safety it
* asked for rather than a value guessed at authoring time.
*/
export const SafeAreaGuides = ({children, guides = true, inset = 0, label = "safe area"}: SafeAreaGuidesProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const layout = useLayout();
const safe = layout.safeArea ?? {x: 80, y: 80};
const x = (safe.x + inset) * scale;
const y = (safe.y + inset) * scale;
const appear = interpolate(frame, [0, 18], [0, 1], {easing: Easing.standard});
return (
<Fill>
<Fill
style={{
alignItems: "center",
justifyContent: "center",
padding: `${y}px ${x}px`,
textAlign: "center",
}}
>
{children}
</Fill>
{guides ? (
<Fill style={{opacity: appear, pointerEvents: "none"}}>
<div
style={{
border: `${2 * scale}px solid ${brand.colors.accent}`,
inset: 0,
margin: `${y}px ${x}px`,
opacity: 0.55,
position: "absolute",
}}
/>
<span
style={{
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 22 * scale,
left: x,
position: "absolute",
top: Math.max(0, y - 34 * scale),
}}
>
{label} · {Math.round(safe.x + inset)} x {Math.round(safe.y + inset)}
</span>
</Fill>
) : null}
</Fill>
);
};
videos/components/safe-area/safe-area.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {SafeAreaGuides} from "./safe-area";
export default defineComponentPreview({
title: "Safe area",
category: "Foundation",
description: "Format-aware title and action safety boundaries.",
component: SafeAreaGuides,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
guides: {type: "boolean", defaultValue: true},
inset: {type: "number", defaultValue: 0, min: 0, max: 160},
label: {type: "text", defaultValue: "safe area"},
},
examples: [
{name: "Guides", props: {guides: true, children: "Everything readable sits inside"}},
{name: "Composed", props: {guides: false, children: "Everything readable sits inside"}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {SafeArea} from "../components/safe-area/safe-area";
<Video>
<Scene id="safe-area" duration="3s">
<SafeArea
guides={true}
inset={0}
label="safe area"
/>
</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
- 90 frames · 3s
- Minimum duration
- 24 frames · 0.8s
- Entrance
- 18 frames
- Exit
- 0 frames
- Reduced motion
- guides appear without fading
- Requires
- sans font, mono font
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | — |
guidesDraw the title and action boundaries. | boolean | true | — |
insetExtra inset beyond the layout policy, in design pixels. | number | 0 | — |
label | string | "safe area" | — |