Media Prompt Bar
The media-gen omnibox
What it is
The media-gen omnibox: a prompt bar purpose-built for image, video, and audio generation. It ships in three presentations — floating over a canvas, docked to the bottom of a workspace, or embedded inside a workflow node — and carries an optional negative-prompt field and settings strip alongside the main textarea.
Why it matters
Freepik, ElevenLabs Flows, CapCut, Playground, and Runway all converge on the same shape: one prompt bar that adapts to where generation happens rather than three separate composers. The settings strip (A7) lives inside it because model, aspect ratio, and resolution changes all move the price, and the cost chip next to them is the same number quoted in E1's generation panel and E5's run button — showing a different number in D1 than at the moment of spend is the specific failure mode the spec calls out.
Observed in: Freepik · ElevenLabs Flows · CapCut · Playground · Runway
Anatomy
- 1
media-prompt-barRoot. Carriesdata-presentation(floating/docked/node-embedded) anddata-locked. - 2
media-prompt-bar-referenceOptional slot above the field for D2 reference-strip — composed by the caller, not imported here. - 3
media-prompt-bar-fieldWraps the textarea and its toolbar. Absent entirely whenlocked. - 4
media-prompt-bar-chipsOptional slot inline above the textarea for D3 context-chips — composed by the caller, not imported here. - 5
media-prompt-bar-textareaThe prompt input. Carries the accessible name, visible or not. - 6
media-prompt-bar-negativeThe negative-prompt row, toggled open in place below the main prompt. Never rendered whenpresentationis node-embedded. - 7
media-prompt-bar-toolbarThe row beneath the textarea: attach, negative-prompt toggle, settings, cost on the left; submit on the right. - 8
media-prompt-bar-attachIcon-only attach button, inside the field. - 9
media-prompt-bar-settingsHosts A7 gen-settings-bar. Collapsed automatically whenpresentationis floating. - 10
media-prompt-bar-costCost chip (M2) — credits at the point of spend, the same number quoted in E1/E5. - 11
media-prompt-bar-submitSend button. Replaced by media-prompt-bar-stop while generating. - 12
media-prompt-bar-stopReplaces submit during generation — a distinct control, not a disabled submit. - 13
media-prompt-bar-paywallReplaces media-prompt-bar-field entirely whenlocked— the gate, in place. - 14
media-prompt-bar-unlockThe paywall's CTA button. - 15
media-prompt-bar-statusVisually hidden live region that announces the generating state.
How to use it
Reach for `presentation="floating"` when the bar sits over a canvas with no other chrome competing for space, `"docked"` as the default anchored composer at the bottom of a generation workspace, and `"node-embedded"` inside a workflow node where the negative prompt has no room and is dropped automatically. Drive `locked` from a real plan/quota check, not from a disabled textarea. Pass `settings` as a composed A7 gen-settings-bar rather than reinventing model/aspect/resolution controls here, and keep the `cost` prop in sync with whatever `settings` currently selects — it should read the same number a user sees at the moment they actually spend it.
Do
Swap the input row for the paywall CTA in place when locked — the composer itself stays the gate, not a redirect to a billing page.
You've hit your plan's limit
Upgrade to keep generating.
Collapse the settings strip in the floating presentation instead of cramming a docked-sized toolbar into a floating bar.
5 credits
Don't
Don't render the paywall as a separate banner beside the bar — it reads as a dismissible ad, not a gate on the action itself.
You've hit your plan's limit
Upgrade to keep generating.
5 creditsDon't keep a negative-prompt toggle in the node-embedded presentation — the canvas has no room for it, so the spec drops it entirely rather than cramming it in disabled.
Accessibility
- Keyboard
- Unlocked and idle, the `docked` bar is four tab stops plus whatever `settings` contributes: the textarea, attach, the negative-prompt toggle, then submit. `node-embedded` drops the negative toggle and is three; `floating` suppresses the settings strip, so it is the same four minus the strip's own stops.
- Opening the negative prompt swaps the toggle for a collapse button inside the new row and adds the negative textarea, so the count stays even but the order changes — the second field and its X now sit between the prompt and the toolbar.
- Enter submits and Shift+Enter breaks the line, in the main prompt only. The negative-prompt textarea binds no key handler, so Enter there inserts a newline.
- There is no IME guard on the submit key. The handler checks only `event.key === "Enter" && !event.shiftKey`, so the Enter that commits a Japanese, Chinese or Korean composition submits the prompt mid-word. K2 `inline-generate-popup` guards this with `event.nativeEvent.isComposing`; this component does not.
- `generating` disables the textarea, attach, and both negative-prompt controls, and replaces submit with stop. Stop is the only control in the bar you can reach while a run is in flight.
- Submit is disabled whenever the trimmed prompt is empty, so an empty bar has one fewer reachable control than a filled one. `locked` collapses the field to two elements, only one of which — the unlock button — is focusable.
- Escape does nothing anywhere. It does not close the negative-prompt row and it does not stop a run.
- Screen reader
- The prompt field is named twice — an sr-only `<label htmlFor>` and an `aria-label` carrying the same `label` string. `aria-label` wins, so the label element is dead weight rather than a second announcement; the two must not be allowed to drift apart.
- The root carries `aria-busy` while `generating`, and `media-prompt-bar-status` is a visually hidden `role="status" aria-live="polite"` region announcing `generatingLabel`. It is mounted in every state holding an empty string, which is what makes the first announcement actually fire.
- Nothing announces the end of a run. The region empties when `generating` goes false, and emptying a live region announces nothing — so "Generating…" is spoken and the finish is silent. Announce the result on the surface that renders it.
- Attach, stop and the negative-prompt collapse are icon-only and named by `attachLabel`, `stopLabel` and `negativePromptCollapseLabel`; every glyph in the bar is `aria-hidden`. Translate those props or the buttons stay in English while the placeholder does not.
- The negative field has a real visible `<label>` and a matching `aria-label`, but the row holding it is a plain div with no group role — so nothing announces that the second field belongs to the first. A screen reader meets two textareas with different names, not a prompt and its negation.
- `locked` swaps the whole field out for the paywall with no live region on the change, so a bar that locks while someone is typing in it changes shape silently.
- The cost chip is static text. Its number is announced when it is passed over and never when it changes.
- Focus
- Starting a run disables the textarea, which is where focus almost certainly was. A disabled element cannot hold focus, so focus falls to `<body>` and the next Tab restarts from the top of the page — at exactly the moment the only useful control is the Stop button that just appeared. Move focus to `media-prompt-bar-stop` in the same handler that sets `generating`.
- Collapsing the negative prompt unmounts the X that had focus, with nothing restoring it. Same failure, smaller blast radius.
- Switching `locked` on unmounts the entire field, textarea included, and nothing catches focus on the way past.
- The textarea's own ring is switched off (`focus-visible:ring-0`) because the container is meant to carry the styling, but no presentation adds a focus-within treatment — so apart from the caret there is no visible indication that the prompt field holds focus. Every button in the bar uses the standard Button ring.
Watch out for
- Deriving
lockedfrom a disabled textarea instead of swapping in the paywall row — a greyed-out composer with no explanation reads as broken, not as an upsell. - Letting the
costprop go stale whensettingschanges — E1, E5, and D1 are all supposed to read from one price; a stale number here is the exact two-prices failure the spec warns about. - Expecting
negativePromptto render whenpresentationis node-embedded — it's forced closed there on purpose, so gate the toggle upstream instead of fighting the component. - Building a separate reference-strip (D2) or context-chips (D3) integration instead of passing them through the
referenceStrip/contextChipsslots — those components may ship independently, and the slots exist precisely so D1 doesn't need to import them directly.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/media-prompt-bar.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.