Hero Omnibox
The centred "What can I help you with?" prompt
What it is
The centred prompt that greets a user on an app's home screen — a large textarea in a card, with attach, model select, and submit built into the field itself. The whole card is clickable, not just the textarea inside it.
Why it matters
Five of five reference App Home screens open with exactly this shape — Descript, Zapier Copilot, CapCut, Manus, and Claude all lead with a centred composer rather than a dashboard or a list. It is the first interactive surface a new session sees, so its states carry weight beyond their own bounds: `locked` is the moment a paid feature gets explained, and `generating` is the first proof the product is actually doing something.
Observed in: Descript · Zapier Copilot · CapCut · Manus · Claude
Anatomy
- 1
hero-omniboxRoot card. Click anywhere on it that isn't a nested control to focus the field. - 2
hero-omnibox-modesOptional mode tabs — composes ModeTabs (D4), rendered inside the card, above the field. - 3
hero-omnibox-fieldWraps the textarea and its toolbar. Absent entirely whenlocked. - 4
hero-omnibox-textareaThe prompt input. Carries the accessible name, visible or not. - 5
hero-omnibox-toolbarThe row beneath the textarea: attach, model, cost on the left; submit on the right. - 6
hero-omnibox-attachIcon-only attach button, inside the field. - 7
hero-omnibox-modelOptional model select, inside the field. - 8
hero-omnibox-costOptional cost chip (M2) — credits at the point of spend, not in a billing page. - 9
hero-omnibox-submitIcon-only send button. Replaced by hero-omnibox-stop while generating. - 10
hero-omnibox-stopReplaces submit duringgenerating— a distinct control, not a disabled submit. - 11
hero-omnibox-paywallReplaces hero-omnibox-field entirely whenlocked— the gate, in place. - 12
hero-omnibox-unlockThe paywall's CTA button. - 13
hero-omnibox-statusVisually hidden live region that announces the generating state.
How to use it
Reach for it as the anchor of an App Home screen — the first thing a user sees when they arrive with no session in progress yet. Drive its `state` prop from real product state (`idle` while empty, `focused` when the field has attention, `generating` while a request is in flight, `locked` once a plan limit is hit) rather than deriving it purely from DOM focus, since `generating` and `locked` are business states, not interaction states. Pass `modes`, `models`, and `cost` only when the surface actually has them — each is optional and the field lays out correctly without any of them.
Do
Swap the textarea for the paywall CTA in place when locked — the composer itself stays the gate.
You've hit your plan's limit
Upgrade to keep generating.
Keep attach, model select, and cost together inside the field's toolbar, not stacked above the card.
5 credits
Don't
Don't render the paywall as a separate banner next to the composer — it reads as a dismissible ad, not a gate on the action itself.
You've hit your plan's limit
Upgrade to keep generating.
Don't gate by disabling the textarea and buttons — a greyed-out field with no explanation reads as broken, not as an upsell.
Accessibility
- Keyboard
- Enter submits and Shift+Enter inserts a newline. Submit is a no-op on an empty or whitespace-only value, while `generating` and while `locked` — the same guard the button uses.
- The submit button is `disabled` until the field holds non-whitespace content, so the composer gains a tab stop as you type the first character.
- `generating` disables the textarea, the attach button, the model select and the mode tabs, leaving Stop as the only focusable control in the card. `locked` unmounts the field entirely, leaving only the paywall CTA.
- Mode tabs are one tab stop, not one per mode. The Base UI toggle group underneath does roving focus, so arrow keys move between modes, Home and End jump to the ends, and the set wraps.
- Clicking anywhere on the card focuses the field. That is mouse-only — the root is a plain `<div>` with a click handler and no key handler — which is harmless here, because the field it focuses is the next tab stop anyway.
- Screen reader
- The field carries both an `sr-only` `<label>` and an `aria-label` holding the same string, and `aria-label` wins. Both come from `label`, so the name moves in one place.
- `generating` is announced. The `role="status"` region is always mounted and only its text is swapped in, which is what makes the announcement land, and the root also carries `aria-busy`. The end of generating is not announced — the text is emptied, and emptying a live region says nothing.
- `locked` announces nothing at all. The field is replaced by the paywall with no live region and no focus move, so a screen-reader user who was typing learns about the limit only by tabbing into it.
- Attach, submit and stop are icon-only buttons with their glyphs `aria-hidden`, named from `attachLabel`, `submitLabel` and `stopLabel`. Stop is a distinct control replacing submit rather than a disabled submit, so it announces as "Stop generating" and not as an unavailable send.
- The model select is named "Model" by a hard-coded `aria-label`. It is not one of the label props, so a surface picking something other than a model has no way to rename it.
- The cost chip is inert text in a `dir="ltr"` span. It is not part of any control's name, so the price is reached only by reading through the toolbar, never by tabbing to it.
- Focus
- The worst case is the ordinary one. Pressing Enter to submit sets `generating`, which disables the textarea you were typing in; focus is dropped from the disabled element and falls to `<body>`, so the next Tab restarts at the top of the page and Stop — the only live control left — is nowhere near it. Move focus to Stop when you flip the state.
- `locked` does the same thing more thoroughly, unmounting the whole field. Move focus to the unlock CTA.
- `autoFocus` is wired to `state === "focused"`, and `autoFocus` only applies at mount: rendering the omnibox already in that state steals focus on arrival, and switching into it later does nothing at all.
- The card draws its ring from the `focused` state — a prop you set, not a `focus-visible` style — while the textarea's own ring is turned off with `focus-visible:ring-0` on the assumption the card carries it. Drive `state` from real focus, or the field has no focus indicator.
Watch out for
- Treating
generatingas a purely visual spinner state. Screen reader users need the live-region announcement — that's why hero-omnibox-status always renders, just invisible until there's something to say. - Placing mode tabs, attach, or model select above the card instead of inside the field's toolbar — it breaks the 'whole card is the control' framing and drifts from the slot arrangement this component shares with D1.
- Burying the cost chip in a settings or billing surface instead of passing it as
cost— the point of a spend indicator is that it sits where the spend happens. - Deriving
generatingorlockedfrom local UI state (like a hover or focus handler) instead of the actual request/plan state — these two states represent real backend conditions, not transient interaction.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/hero-omnibox.jsonRequires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.