Command Menu
A keyboard-driven command palette with backdrop, typing animation, and row selection
A state atom representing a command palette panel. Its appearance (opened, closed) is a pure function of the state prop. Pass a state directly or drive it from the Remotion timeline via useCurrentState. The component reads no frame — only the state value you give it. The panel zooms in from 0.96 scale with a backdrop dim and fades fully in on open; reverses on close.
Installation
$ pnpm dlx shadcn@latest add @remocn/command-menuInstalling command-menu automatically installs the shared remocn-ui core (lib/remocn-ui/) and the command-menu-item atom via registryDependencies (["remocn-ui", "command-menu-item"]). You do not need to install either separately.
States
CommandMenuState is:
type CommandMenuState =
| "opened" // backdrop opaque, panel opacity 1, scale 1, translateY 0
| "closed" // backdrop transparent, panel opacity 0, scale 0.96, translateY 8pxopened reveals the full panel with backdrop dim. closed hides the panel and clears the backdrop.
Snap usage
Pass state directly — the component snaps instantly to that visual. Useful for static previews or when you drive state from your own logic:
import { CommandMenu } from "@/components/remocn/command-menu";
export const Scene = () => (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<CommandMenu state="opened" query="set" />
</div>
);To drive state from the timeline, use useCurrentState:
import { useCurrentState } from "@/lib/remocn-ui";
import { CommandMenu } from "@/components/remocn/command-menu";
export const Scene = () => {
const state = useCurrentState(
[
{ at: 16, state: "opened" },
{ at: 96, state: "closed" },
],
"closed",
);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<CommandMenu state={state} />
</div>
);
};Each at is a Sequence-local authored frame. State persists between steps: opened at frame 16 keeps the panel visible until closed fires at frame 96. See Concepts for the full useCurrentState API.
Smooth transitions
State changes via state snap with no cross-fade. For an animated open/close, use useCommandMenuTransition from the copied use-command-menu-transition.ts file. It reads the frame, interpolates between state presets, and returns a resolved CommandMenuStyle — pass it to the style prop:
import { CommandMenu } from "@/components/remocn/command-menu";
import { useCommandMenuTransition } from "@/components/remocn/use-command-menu-transition";
export const Scene = () => {
const style = useCommandMenuTransition([
{ at: 16, state: "opened", duration: 16 },
{ at: 96, state: "closed", duration: 12 },
]);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<CommandMenu style={style} />
</div>
);
};The duration field on each step overrides the file's DEFAULT_DURATION (= 12) for that specific transition. To globally tune timing and easing, edit use-command-menu-transition.ts directly in your project — that file is yours (shadcn "own your code" philosophy).
style takes precedence over state when both are provided.
Typing and filtering
The query prop holds the full search string. The revealCount prop controls how many characters of query are currently shown — the panel filters the item list against the visible prefix and shows an empty state when nothing matches.
Use revealCount from @/lib/remocn-ui to compute the reveal from the timeline frame:
import { useCurrentFrame } from "remotion";
import { revealCount } from "@/lib/remocn-ui";
import { CommandMenu } from "@/components/remocn/command-menu";
import { useCommandMenuTransition } from "@/components/remocn/use-command-menu-transition";
const QUERY = "settings";
export const Scene = () => {
const frame = useCurrentFrame();
const panelStyle = useCommandMenuTransition([
{ at: 16, state: "opened", duration: 16 },
{ at: 108, state: "closed", duration: 12 },
]);
// 8 chars at 4 chars/sec @ 30 fps = 60 frames total typing window.
const revealed = revealCount(
Math.max(0, frame - 20), // typing starts at frame 20
30, // fps
QUERY.length, // total characters
4, // chars per second
);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<CommandMenu style={panelStyle} query={QUERY} revealCount={revealed} />
</div>
);
};revealCount(localFrame, fps, len, cps) is a pure function — it returns the number of characters to show given the elapsed local frame, fps, total query length, and typing speed in chars per second. Import it from @/lib/remocn-ui.
When revealCount is omitted, the full query is shown immediately. When the visible prefix is empty, all items are shown. The pure filter is exported as filterCommandItems(items, query, revealCount) if you need it in your own logic.
Items
The panel renders a column of CommandMenuItem rows — one per filtered entry. Each row has four states reflecting the user's interaction:
CommandMenuItemState is:
type CommandMenuItemState =
| "idle" // popover background, muted icon, normal label
| "hover" // accent background, foreground icon, normal label
| "press" // darker accent background, foreground icon, scale 0.98
| "selected" // accent background, accent-foreground label, foreground iconidle rests on the popover background. hover brightens to the accent color. press darkens slightly and shrinks to 0.98 scale. selected locks the accent background with an accent-foreground label.
Preview a single CommandMenuItem:
When you install command-menu, the command-menu-item atom is automatically pulled in via registryDependencies — no separate install needed. The CommandMenu container derives each row's state from the selectedIndex, highlightedIndex, and pressedIndex props (indexed into the filtered list), or you can override a row's visual with itemStyles.
To animate a row's state transition, drive it with useCommandMenuItemTransition:
import { useCommandMenuItemTransition } from "@/components/remocn/use-command-menu-item-transition";
const itemStyle = useCommandMenuItemTransition([
{ at: 84, state: "hover", duration: 8 },
{ at: 92, state: "press", duration: 6 },
{ at: 100, state: "selected", duration: 8 },
]);
// Pass to CommandMenu via itemStyles (indexed into the filtered list):
<CommandMenu style={panelStyle} itemStyles={[itemStyle]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
state | "opened" | "closed" | "closed" | Current visual state (snap path). State changes snap — no automatic cross-fade. |
style | CommandMenuStyle | — | Resolved animated visual (smooth path). Pass an interpolated CommandMenuStyle from useCommandMenuTransition. Takes precedence over state when provided. |
query | string | "" | Full search query text. The visible prefix is query.slice(0, revealCount). The item list is filtered against this prefix. |
revealCount | number | — | Number of query characters revealed so far. Omitted means the whole query is shown immediately. Use the revealCount() helper from @/lib/remocn-ui to compute this from the frame. |
items | CommandMenuEntry[] | [Profile, Settings, New File, Search docs] | The full list of command rows before filtering. Each entry has label (required), optional icon (search/settings/user/file), and optional shortcut string. |
selectedIndex | number | -1 | Index into the FILTERED list of the persisted selection. -1 for none. |
highlightedIndex | number | -1 | Index into the FILTERED list of the hovered row. -1 for none. |
pressedIndex | number | -1 | Index into the FILTERED list of the pressed row. -1 for none. |
itemStyles | (CommandMenuItemStyle | undefined)[] | — | Per-row resolved visual override (smooth path), indexed into the FILTERED list. When present it wins over the index-to-state derivation. Used by the live example to animate a single row. |
theme | Partial<RemocnTheme> | — | Per-component theme override. Merges with the active RemocnUIProvider theme and the mode defaults. Must be concrete oklch/hex/rgb values — not CSS custom properties. |
mode | "light" | "dark" | "light" | Sets the base palette. Overrides the mode on RemocnUIProvider when both are present. |
className | string | — | Optional className applied to the panel element. |