NbFloatingToolbar is a small bar of controls that floats next to something on the page: formatting buttons over a text selection, actions over a selected image, alignment controls over a table cell. It holds ordinary NbButtons, places itself with the same flip-and-clamp logic as NbInfoHint, and is careful never to take focus from the thing it acts on.
Select some text in the paragraph below.
Anchoring to a selection
A text selection has a rectangle but no element, which is why anchor accepts three shapes:
| Shape | Example | Behaviour |
|---|---|---|
| An element | buttonEl | Re-measured on every reposition |
| A virtual anchor | { getBoundingClientRect: () => rect } | Called on every reposition, so it stays live |
| A plain rectangle in viewport pixels | { top, left, width, height } or a DOMRect | A snapshot. Pass a new one when it moves |
For a selection, a virtual anchor is the one to reach for. The toolbar calls it again on scroll and resize, so it follows the text while the page scrolls under it.
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
import type { TFloatingToolbarAnchor } from '@nubisco/ui'
const open = ref(false)
const anchor = shallowRef<TFloatingToolbarAnchor | null>(null)
const toolbar = ref()
function onSelectionChange() {
const selection = document.getSelection()
if (!selection || selection.isCollapsed || !selection.rangeCount) {
open.value = false
return
}
const range = selection.getRangeAt(0)
anchor.value = { getBoundingClientRect: () => range.getBoundingClientRect() }
open.value = true
toolbar.value?.reposition()
}
onMounted(() => document.addEventListener('selectionchange', onSelectionChange))
onBeforeUnmount(() =>
document.removeEventListener('selectionchange', onSelectionChange),
)
</script>
<template>
<NbFloatingToolbar
ref="toolbar"
:open="open"
:anchor="anchor"
label="Text formatting"
>
<NbButton size="sm" variant="ghost" icon="text-b" aria-label="Bold" />
<NbButton
size="sm"
variant="ghost"
icon="text-italic"
aria-label="Italic"
/>
</NbFloatingToolbar>
</template>open is entirely yours. The toolbar never opens or closes itself: it reports Escape through close and update:open, and your state decides.
Placement
placement is a preference. A toolbar over a selection on the first line of a page has no room above it, so it flips below, then to the sides, and finally clamps itself inside the viewport. When the anchor scrolls entirely out of view the toolbar hides rather than sitting clamped against the edge of the window, acting on text nobody can see.
Focus
Three rules, all of which exist because the toolbar's main consumer is a text editor:
- Appearing never moves focus. The editor keeps the caret and the selection.
- Pressing a button never moves focus. The toolbar cancels
mousedown, which is the event that would focus the button and blur the editor. Theclickstill fires. Text fields inside the toolbar (a link URL, say) are exempt, since they genuinely need focus. - Keyboard users get in deliberately. Call the exposed
focus()from your editor's shortcut (Alt+F10 is the common one), and return focus to the editor yourself onclose.
Keyboard
The toolbar is a single tab stop, following the WAI-ARIA toolbar pattern.
| Key | Does |
|---|---|
| → / ← | Next or previous control, wrapping (horizontal) |
| ↓ / ↑ | Next or previous control, wrapping (vertical) |
| Home / End | First or last control |
| Tab | Leaves the toolbar |
| Esc | Emits close |
Disabled controls are skipped. Arrow keys inside a text field move the caret, not the toolbar.
Accessibility
role="toolbar", named by the requiredlabeland oriented byaria-orientation.- Roving
tabindexis applied to the slotted controls directly, so plainNbButtons work without any extra wiring. It is kept up to date as controls are added, removed or disabled. - Icon-only buttons need their own accessible name. Pass
aria-labelto eachNbButton, as in the examples. - The entrance animation is suppressed under
prefers-reduced-motion: reduce.