Selection Toolbar
The ✨ menu on selected text
It is our belief that the current onboarding process is not optimal and could probably be improved in a number of different ways going forward.
Pick a verb. The toolbar asks; it never edits the paragraph.
What it is
The floating bar that appears over selected prose and offers AI writing verbs: Improve first and filled, then Shorten, Expand, a Tone submenu, and a free-text Custom prompt. It is the context toolbar with a writing vocabulary — same eight-action ceiling, same pinned first entry, same above/below placement — and it emits a request rather than performing an edit.
Why it matters
This is the most copied AI interaction in writing tools: Notion AI, Spellbook, Google Docs and Canva Docs all put the same bar over the same selection, and users arrive already knowing what the sparkle means. What separates a trustworthy one from an alarming one is what happens next. The bar asks; the rewrite comes back as a diff the reader can reject. A toolbar that quietly replaced the paragraph would be indistinguishable from a bug, so this one has no write path at all — no value in, no text out, only an intent.
Observed in: Notion AI · Spellbook · Google Docs · Canva Docs
Anatomy
- 1
selection-toolbarRoot. Carries data-pending, data-surface and aria-busy. - 2
context-toolbarThe bar itself, from I3 — role=toolbar, roving focus, overflow, placement. - 3
context-toolbar-aiImprove. I3's AI slot: index 0, filled, never eligible for overflow. - 4
context-toolbar-actionThe plain verbs — Shorten, Expand, and any actions you add. - 5
selection-toolbar-toneThe Tone verb; one button, not one per tone. - 6
selection-toolbar-tone-menuThe tone submenu that opens behind it. - 7
selection-toolbar-tone-itemOne tone; carries data-tone with its id. - 8
selection-toolbar-customThe Custom prompt verb. - 9
selection-toolbar-promptThe free-text popover — a dialog named by its title, not a navigation. - 10
selection-toolbar-prompt-inputThe instruction textarea. Cmd/Ctrl+Enter submits. - 11
selection-toolbar-prompt-submitSubmit; disabled while the instruction is empty. - 12
selection-toolbar-statusLive region that announces the in-flight verb and says the result will be reviewable.
How to use it
Reach for it wherever a user can select prose and ask a model to change it — a document editor, a comment box, a long-form field. Give it the selected text and a handler, and treat the handler as the start of a request: run the model, then render the result as a K3 `diff-review` beside the original. Set `pending` while that request is in flight so the bar stays put and says what it is doing. Tone lives in a submenu here because the bar has room for one button; in a generation panel the same list belongs in an E4 `preset-grid`, where a visual grid earns the space it takes.
Do
Keep Improve first and filled and leave every other verb as plain text — the hierarchy is what makes the bar readable at a glance.
Return the rewrite as a reviewable change with a rationale, and show the bar as pending until it lands.
Shortening the selection. The result will arrive as a change you can review.Proposed change · shorten
It is our belief that the onboarding process is not optimal Onboarding needs work and could probably be improved.
Removed hedging; kept the claim. Accept or reject per change.
Don't
Don't swap the paragraph in place. A silent rewrite leaves nothing to compare against and no way to reject it.
Rewritten
Onboarding needs work and could be improved.
No original, no rationale, no way back.
Don't spread tone across the bar. Five tone buttons are one decision wearing five buttons, and they spend the action budget a product needs for its own verbs.
ImproveProfessionalFriendlyCasualConfidentDirect
Accessibility
- Keyboard
- The whole bar is **one** tab stop, however many verbs are in it. It is a `role="toolbar"` with a roving tabindex, so Tab enters and leaves; Left and Right move between buttons inside, and they wrap at both ends.
- Order along the bar is fixed: Improve, then Shorten and Expand and any `actions` you added, then Tone, then Custom prompt, then the overflow trigger. Tone and Custom prompt sit before overflow because they are children of the bar rather than rows in `actions`.
- Space and Enter activate a verb. Tone opens a menu — Up and Down move through tones, typeahead jumps, Escape closes and returns focus to the Tone button. Custom prompt opens a popover; Escape closes that too.
- In the prompt popover, Cmd/Ctrl+Enter submits and plain Enter inserts a newline. The Submit button is natively `disabled` while the instruction is empty, so it is not focusable at all until you have typed something.
- `pending` does **not** remove the verbs from keyboard travel. Base UI's toolbar buttons stay focusable when disabled and report `aria-disabled="true"` rather than the native attribute, so an in-flight bar can still be arrowed through and its buttons still take focus — they simply refuse to fire.
- Screen reader
- The bar announces as a toolbar named "AI writing tools" by default, overridable through `label`. That name matters more than it looks: a canvas can hold several toolbars and the name is the only thing that tells them apart.
- Improve, Shorten and Expand draw their labels, and every icon — including the in-flight spinner, which replaces the icon inside the same `aria-hidden` wrapper — is hidden from assistive tech. So a button's name does not change while its request is running: "Shorten" stays "Shorten", now `aria-disabled`.
- `selection-toolbar-status` is an always-mounted `role="status"` region and is the only thing that reports work starting. It announces the verb and the contract in one breath — "Shortening the selection. The result will arrive as a change you can review" — which is deliberate: without the second sentence a user is left expecting their paragraph to change underneath them.
- If your host never sets `pending`, that region stays empty and choosing a verb is announced as nothing at all. The bar has no other feedback path.
- The custom-prompt popover is a dialog named by its title, with the quoted selection as its description — so entering it announces "Custom instruction" and then the snippet being rewritten. The textarea takes its name from `promptPlaceholder` via `aria-label`, which is why the placeholder and the name are the same string rather than the placeholder standing in for a label.
- The tone submenu is a menu of `menuitem`s named by their tone labels. Nothing marks the current tone as chosen — this is a menu that fires an intent, not a set of radios, so no tone is ever reported as selected.
- Focus
- Closing either popup returns focus to the button that opened it — the Tone button after picking a tone, the Custom prompt button after submitting. That is Base UI's doing and it is correct.
- The bar does not take focus when it appears over a selection. A keyboard user has to Tab to it, and where it lands in the tab order is wherever your editor renders it.
- Nothing here moves focus to the result. When the rewrite comes back as a `diff-review`, moving focus to it is the host's job.
- `pending` arriving while a verb has focus does not steal it, because the buttons are `aria-disabled` rather than natively disabled. This is the one place in the family where a busy state does not drop focus to `<body>`.
- Every button is a vendored `Button` and ships a `focus-visible` ring.
Watch out for
- Expecting the bar to position itself. Like the context toolbar underneath it, this component expresses
placementbut never measures the viewport — the editor knows where the selection is, so the editor decides above or below and must keep the bar off the text it belongs to. - Forgetting that Tone and Custom prompt occupy two of the eight slots. Extra
actionsoverflow into the menu sooner than you would expect, which is deliberate: the ceiling counts buttons the user can see, not rows you passed in. Passtones={[]}if your product has no tone list and you want the slot back. - Treating
pendingas a lock on your own request queue. It dims and refuses the bar's verbs, but the host still owns cancellation — if the user selects different text mid-request, drop the stale response rather than applying it to the new selection. - Reading the custom prompt as navigation to a bigger surface. It is a popover with a textarea, submitted in place; sending the user to a side panel to type one sentence loses the selection that made the sentence make sense.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/selection-toolbar.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.