Claude
A Claude composer with an animated prompt.
Customize
import {Scene, Video} from "odori";
import {Claude} from "../components/claude/claude";
<Video>
<Scene id="claude" duration="6s">
<Claude
prompt="Summarize the incident and draft the customer note."
placeholder="How can I help you today?"
model="Claude Opus 4.5"
theme="dark"
charactersPerSecond={26}
/>
</Scene>
</Video>Installation
pnpm odori add component claudenpx odori add component claudebunx odori add component claudeEdit the source in videos/components/claude/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/claude/claude.tsx
import {Easing, Fill, interpolate, useBrand, useDesignScale, useFrame, useTyping} from "odori";
import type {ReactNode} from "react";
export type ClaudeProps = {
/** The prompt that gets typed. */
prompt: string;
/** What sits in the field before the first character. */
placeholder?: string;
/** The model chip under the field. An empty string hides it. */
model?: string;
/**
* A mark drawn above the composer. Nothing ships here: pass a mark you have
* the right to use, or leave it out and let the surface speak.
*/
logo?: ReactNode;
theme?: "dark" | "light";
charactersPerSecond?: number;
};
/**
* The surface carries its own palette rather than the brand's: a composer
* recoloured to your product is no longer a picture of the assistant. Type is
* the exception, because the brand's sans keeps the cut coherent and nobody
* reads a UI screenshot for its typeface.
*/
const PALETTE = {
dark: {page: "#151515", field: "rgba(255,255,255,0.06)", edge: "#302F2C", ink: "#F0EFEC", faint: "#8F8B80", accent: "#C96442"},
light: {page: "#FCFCFB", field: "#FFFFFF", edge: "#E3DFD4", ink: "#0B0B0B", faint: "#8A8578", accent: "#C96442"},
};
/** Claude sets its interface in anthropic-sans on a near-black ground. */
const FONT = '"anthropic-sans", ui-sans-serif, system-ui, -apple-system, sans-serif';
/**
* An assistant composer with a prompt landing in it.
*
* The whole surface is one frame-driven state: characters appear, and the
* button on the right turns over from the idle voice control into a send
* arrow the moment there is something to send. That turn is the shot.
* Everything else holds still so the eye has one thing to follow.
*/
export const Claude = ({
prompt,
placeholder = "How can I help you today?",
model = "Claude Opus 4.5",
logo,
theme = "dark",
charactersPerSecond = 26,
}: ClaudeProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const typed = useTyping(prompt, {from: 14, charactersPerSecond, chunk: 2});
const colors = PALETTE[theme];
const px = (value: number) => value * scale;
const enter = interpolate(frame, [0, 16], [0, 1], {easing: Easing.standard});
// The send control turns over once, over six frames from the first
// character, and stays turned.
const turn = interpolate(typed.length > 0 ? frame : 0, [14, 20], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", background: colors.page, justifyContent: "center", padding: px(120)}}>
<div
style={{
maxWidth: px(1180),
opacity: enter,
transform: `translateY(${(1 - enter) * px(18)}px)`,
width: "100%",
}}
>
{logo ? <div style={{marginBottom: px(34)}}>{logo}</div> : null}
<div
style={{
background: colors.field,
/* Claude rings the composer with a half-pixel line and a soft
drop, not a full border: measured as 0 4px 20px at 7% over
0 0 0 0.5px at 30%. A 1px border reads harder than either. */
border: theme === "dark" ? `${px(1)}px solid ${colors.edge}` : undefined,
borderRadius: px(30),
boxShadow:
theme === "dark"
? `0 ${px(24)}px ${px(70)}px rgba(0,0,0,0.42)`
: `0 ${px(6)}px ${px(30)}px rgba(0,0,0,0.07), 0 0 0 ${px(0.8)}px rgba(33,33,30,0.30)`,
padding: `${px(30)}px ${px(30)}px ${px(22)}px`,
}}
>
<div
style={{
color: typed.length > 0 ? colors.ink : colors.faint,
fontFamily: FONT,
fontSize: px(34),
letterSpacing: "-0.01em",
lineHeight: 1.33,
minHeight: px(96),
whiteSpace: "pre-wrap",
}}
>
{typed.length > 0 ? typed.text : placeholder}
{typed.caret ? (
<span
style={{
background: colors.accent,
display: "inline-block",
height: px(34),
marginLeft: px(4),
transform: `translateY(${px(5)}px)`,
width: px(3),
}}
/>
) : null}
</div>
<div style={{alignItems: "center", display: "flex", justifyContent: "space-between", marginTop: px(18)}}>
{model ? (
<span
style={{
border: `${px(1)}px solid ${colors.edge}`,
borderRadius: px(999),
color: colors.faint,
fontFamily: FONT,
fontSize: px(20),
padding: `${px(8)}px ${px(16)}px`,
}}
>
{model}
</span>
) : (
<span />
)}
{/* One control, two faces. The idle face fades and shrinks as the
armed face grows through it, so the turn reads as the same
button changing its mind rather than two buttons swapping. */}
<span
style={{
alignItems: "center",
background: `color-mix(in srgb, ${colors.accent} ${turn * 100}%, transparent)`,
border: `${px(1)}px solid color-mix(in srgb, ${colors.accent} ${turn * 100}%, ${colors.edge})`,
borderRadius: px(999),
display: "inline-flex",
height: px(60),
justifyContent: "center",
position: "relative",
width: px(60),
}}
>
<span
style={{
alignItems: "center",
display: "flex",
gap: px(4),
opacity: 1 - turn,
position: "absolute",
transform: `scale(${1 - turn * 0.3})`,
}}
>
{[14, 22, 16].map((height, index) => (
<span
key={index}
style={{background: colors.faint, borderRadius: px(999), height: px(height), width: px(3)}}
/>
))}
</span>
<svg
viewBox="0 0 24 24"
width={px(26)}
height={px(26)}
style={{opacity: turn, position: "absolute", transform: `scale(${0.7 + turn * 0.3})`}}
>
<path
d="M12 19V5M12 5l-6 6M12 5l6 6"
fill="none"
stroke="#FFFFFF"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.4}
/>
</svg>
</span>
</div>
</div>
</div>
</Fill>
);
};
videos/components/claude/claude.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {Claude} from "./claude";
export default defineComponentPreview({
title: "Claude",
category: "Agents",
description: "A Claude composer with an animated prompt.",
component: Claude,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
prompt: {type: "text", defaultValue: "Summarize the incident and draft the customer note.", maxLength: 140},
placeholder: {type: "text", defaultValue: "How can I help you today?", maxLength: 48},
model: {type: "text", defaultValue: "Claude Opus 4.5", maxLength: 28},
theme: {type: "select", defaultValue: "dark", options: ["dark", "light"]},
charactersPerSecond: {type: "number", defaultValue: 26, min: 8, max: 60, step: 1},
},
examples: [
{name: "Default", props: {}},
{name: "Light", props: {theme: "light"}},
{name: "Bare", props: {model: "", prompt: "Ship it."}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Claude} from "../components/claude/claude";
<Video>
<Scene id="claude" duration="6s">
<Claude
prompt="Summarize the incident and draft the customer note."
placeholder="How can I help you today?"
model="Claude Opus 4.5"
theme="dark"
charactersPerSecond={26}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Agents
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 90 frames · 3s
- Entrance
- 16 frames
- Exit
- 12 frames
- Reduced motion
- no button turn, send state from the first frame
- Requires
- sans font
- Content limits
- prompt ≤ 140, placeholder ≤ 48, model ≤ 28
Props
| Prop | Type | Default | Required |
|---|---|---|---|
promptThe prompt that gets typed. | string | — | Yes |
placeholderWhat sits in the field before the first character. | string | "How can I help you today?" | — |
modelThe model chip under the field. An empty string hides it. | string | "Claude Opus 4.5" | — |
logoA mark drawn above the composer. Nothing ships here: pass a mark you have the right to use, or leave it out and let the surface speak. | ReactNode | — | — |
theme | "dark" | "light" | "dark" | — |
charactersPerSecond | number | 26 | — |
Related components
All componentsChatGPTA ChatGPT composer with an animated prompt.Claude CodeA Claude Code session from prompt to response.fxA coding agent session in a terminal.OpenCodeAn open coding agent's terminal session, with the query typed under its wordmark.v0A v0 composer with an animated prompt.Absolute stageFull-frame surface with stable clipping and positioning.