Combobox

A searchable select with a typed-query input, filtered panel, and row selection

A state atom representing a searchable select: a text-input trigger and a panel of filtered options. 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 reveals below the trigger with opacity, scale, and vertical lift animations. The trigger reuses the Input visual presets; the panel rows reuse the SelectItem atom.

Installation

$ pnpm dlx shadcn@latest add @remocn/combobox

Installing combobox automatically installs the shared remocn-ui core (lib/remocn-ui/), the input primitive, and the select-item atom via registryDependencies (["remocn-ui", "input", "select-item"]). You do not need to install them separately. The trigger renders from Input's visual presets, and the panel renders a column of SelectItem rows.

States

ComboboxState is:

type ComboboxState =
  | "opened"  // panel opacity 1, scale 1, translateY 0
  | "closed"  // panel opacity 0, scale 0.96, translateY -4px

opened reveals the dropdown panel below the trigger. closed hides it.

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 { Combobox } from "@/components/remocn/combobox";

export const Scene = () => (
  <div style={{ position: "relative", width: "100%", height: "100%" }}>
    <Combobox state="opened" query="ba" />
  </div>
);

To drive state from the timeline, use useCurrentState:

import { useCurrentState } from "@/lib/remocn-ui";
import { Combobox } from "@/components/remocn/combobox";

export const Scene = () => {
  const state = useCurrentState(
    [
      { at: 16, state: "opened" },
      { at: 96, state: "closed" },
    ],
    "closed",
  );

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox state={state} />
    </div>
  );
};

Each at is a Sequence-local authored frame. State persists between steps. See Concepts for the full useCurrentState API.

Smooth transitions

State changes via state snap with no cross-fade. For an animated open/close, use useComboboxTransition from the copied use-combobox-transition.ts file. It reads the frame, interpolates between state presets, and returns a resolved ComboboxStyle — pass it to the style prop:

import { Combobox } from "@/components/remocn/combobox";
import { useComboboxTransition } from "@/components/remocn/use-combobox-transition";

export const Scene = () => {
  const style = useComboboxTransition([
    { at: 16, state: "opened", duration: 12 },
    { at: 96, state: "closed", duration: 12 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox 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-combobox-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 option list against the visible prefix and shows a "No results found." 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 { Combobox } from "@/components/remocn/combobox";
import { useComboboxTransition } from "@/components/remocn/use-combobox-transition";

const QUERY = "ba";
const TYPE_START = 32;

export const Scene = () => {
  const frame = useCurrentFrame();

  const style = useComboboxTransition([
    { at: 16, state: "opened", duration: 12 },
    { at: 96, state: "closed", duration: 12 },
  ]);

  // 2 chars at 4 chars/sec @ 30 fps = 15 frames total typing window.
  const revealed = revealCount(
    Math.max(0, frame - TYPE_START), // typing starts at frame 32
    30,                               // fps
    QUERY.length,                     // total characters
    4,                                // chars per second
  );

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox style={style} 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 filterComboboxItems(items, query, revealCount) if you need it in your own logic.

Items

The panel renders a column of SelectItem rows — one per filtered option. The row visual is the same SelectItem atom used by the Select and Dropdown Menu components. See the Select page for the full SelectItemState documentation (idle, hover, press, selected) and the SelectItem atom preview.

When you install combobox, the select-item atom is automatically pulled in via registryDependencies — no separate install needed. The Combobox container derives each row's state from selectedIndex, highlightedIndex, and pressedIndex (all 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 useSelectItemTransition:

import { useSelectItemTransition } from "@/components/remocn/use-select-item-transition";

const itemStyle = useSelectItemTransition([
  { at: 60, state: "hover",    duration: 8 },
  { at: 72, state: "press",    duration: 6 },
  { at: 80, state: "selected", duration: 8 },
]);

// Pass to Combobox via itemStyles (indexed into the FILTERED list):
<Combobox style={panelStyle} itemStyles={[itemStyle]} />

Props

PropTypeDefaultDescription
state
"opened" | "closed""closed"Current visual state (snap path). State changes snap — no automatic cross-fade.
style
ComboboxStyleResolved animated visual (smooth path). Pass an interpolated ComboboxStyle from useComboboxTransition. 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
numberNumber 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.
placeholder
string"Select a fruit…"Trigger placeholder shown before any query is typed.
items
string[]["Apple", "Banana", "Orange", "Grape"]The full option list before filtering. Each entry is a plain string.
selectedIndex
number-1Index into the FILTERED list of the persisted selection. -1 for none.
highlightedIndex
number-1Index into the FILTERED list of the hovered row. -1 for none.
pressedIndex
number-1Index into the FILTERED list of the pressed row. -1 for none.
itemStyles
(SelectItemStyle | undefined)[]Per-row resolved visual override (smooth path), indexed into the FILTERED list. When present it wins over the index-to-state derivation. Drive individual rows with useSelectItemTransition.
inputStyle
InputStyleResolved Input visual for the trigger (smooth path). Drive it with useInputTransition for a focus-ring + typed-text reveal animation. Defaults to the typing look once a query is revealed, else idle.
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
stringOptional className applied to the outer wrapper element.