Identity reveal
An animated product logo and name.
Customize
import {Scene, Video} from "odori";
import {IdentityReveal} from "../components/identity-reveal/identity-reveal";
<Video>
<Scene id="identity-reveal" duration="3s">
<IdentityReveal
wordmark="odori"
detail="An independent React video framework."
/>
</Scene>
</Video>Installation
pnpm odori add component identity-revealnpx odori add component identity-revealbunx odori add component identity-revealEdit the source in videos/components/identity-reveal/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/identity-reveal/identity-reveal.tsx
import type {ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type IdentityRevealProps = {
/** Defaults to the brand name, so a project never hardcodes its own mark. */
wordmark?: string;
/**
* Your own mark, as JSX. Inline SVG scales with the design, takes brand
* colors, and can animate per frame, which a file can never do.
*/
mark?: ReactNode;
/** A key in `brand.logos`, or a URL. Used when no `mark` is given. */
logo?: string;
detail?: string;
};
/**
* The product mark and name entering together: the mark settles first, the
* wordmark follows out of a clipped box, and a rule draws under both. Restraint
* is the point, so nothing spins or bounces.
*/
export const IdentityReveal = ({wordmark, mark: markNode, logo, detail}: IdentityRevealProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const name = wordmark ?? brand.name;
const source = logo ? brand.logos[logo] ?? logo : undefined;
const mark = interpolate(frame, [0, 24], [0, 1], {easing: Easing.standard});
const word = interpolate(frame, [12, 38], [0, 1], {easing: Easing.standard});
const rule = interpolate(frame, [28, 54], [0, 1], {easing: Easing.standard});
const note = interpolate(frame, [38, 60], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", gap: 26 * scale, justifyContent: "center"}}>
<div style={{alignItems: "center", display: "flex", gap: 26 * scale}}>
{markNode ? (
<div
style={{
alignItems: "center",
display: "flex",
height: 88 * scale,
opacity: mark,
transform: `scale(${0.92 + mark * 0.08})`,
}}
>
{markNode}
</div>
) : source ? (
<img alt="" src={source} style={{height: 88 * scale, opacity: mark, transform: `scale(${0.92 + mark * 0.08})`}} />
) : (
<span
style={{
background: brand.colors.accent,
borderRadius: 18 * scale,
display: "block",
height: 72 * scale,
opacity: mark,
transform: `scale(${0.9 + mark * 0.1})`,
width: 72 * scale,
}}
/>
)}
<div style={{overflow: "hidden", paddingBottom: 8 * scale}}>
<span
style={{
display: "inline-block",
fontSize: 96 * scale,
fontWeight: 600,
letterSpacing: "-0.05em",
opacity: word,
transform: `translateY(${(1 - word) * 66 * scale}px)`,
}}
>
{name}
</span>
</div>
</div>
<div style={{background: brand.colors.border, height: Math.max(1, 1 * scale), transform: `scaleX(${rule})`, width: 420 * scale}} />
{detail ? <div style={{color: brand.colors.muted, fontSize: 32 * scale, opacity: note}}>{detail}</div> : null}
</Fill>
);
};
videos/components/identity-reveal/identity-reveal.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {IdentityReveal} from "./identity-reveal";
export default defineComponentPreview({
title: "Identity reveal",
category: "Brand",
description: "An animated product logo and name.",
component: IdentityReveal,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
wordmark: {type: "text", defaultValue: "odori", maxLength: 24},
detail: {type: "text", defaultValue: "An independent React video framework."},
},
examples: [
{name: "Mark and name", props: {}},
{name: "Name only", props: {wordmark: "odori", detail: ""}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {IdentityReveal} from "../components/identity-reveal/identity-reveal";
<Video>
<Scene id="identity-reveal" duration="3s">
<IdentityReveal
wordmark="odori"
detail="An independent React video framework."
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Brand
- Recommended duration
- 90 frames · 3s
- Minimum duration
- 60 frames · 2s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- mark and name fade in together
- Requires
- sans font
- Content limits
- wordmark ≤ 24, detail ≤ 72
Props
| Prop | Type | Default | Required |
|---|---|---|---|
wordmarkDefaults to the brand name, so a project never hardcodes its own mark. | string | — | — |
markYour own mark, as JSX. Inline SVG scales with the design, takes brand colors, and can animate per frame, which a file can never do. | ReactNode | — | — |
logoA key in `brand.logos`, or a URL. Used when no `mark` is given. | string | — | — |
detail | string | — | — |
Related components
All componentsBrand providerThe resolved brand rendered as a sheet: color, type, and motion.Icon setA stroked icon set that draws itself on, as editable SVG source.Logo orbitProduct identity surrounded by connected integrations.Marks starterPlaceholder marks that draw themselves on, as editable SVG source.Absolute stageFull-frame surface with stable clipping and positioning.AccordionSections opening one at a time, the outgoing one closing as the next grows.