Skip to content

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:

ShapeExampleBehaviour
An elementbuttonElRe-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 DOMRectA 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.

vue
<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. The click still 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 on close.

Keyboard ​

The toolbar is a single tab stop, following the WAI-ARIA toolbar pattern.

KeyDoes
→ / ←Next or previous control, wrapping (horizontal)
↓ / ↑Next or previous control, wrapping (vertical)
Home / EndFirst or last control
TabLeaves the toolbar
EscEmits close

Disabled controls are skipped. Arrow keys inside a text field move the caret, not the toolbar.

Accessibility ​

  • role="toolbar", named by the required label and oriented by aria-orientation.
  • Roving tabindex is applied to the slotted controls directly, so plain NbButtons 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-label to each NbButton, as in the examples.
  • The entrance animation is suppressed under prefers-reduced-motion: reduce.

Props ​

PropTypeDefaultDescription
openbooleanfalseRenders the toolbar.
anchorElement | { getBoundingClientRect() } | IAnchorRect | nullnullWhat the toolbar floats next to. Nothing renders without one.
labelstring(required)Accessible name of the toolbar.
placement'top' | 'bottom' | 'left' | 'right''top'Preferred side. Flipped and clamped to stay in the viewport.
gapnumber8Pixels between the anchor and the toolbar.
orientation'horizontal' | 'vertical''horizontal'Layout, arrow-key axis and aria-orientation.
teleportTostring'body'Teleport target.

Events ​

EventPayloadDescription
close(none)Escape was pressed inside the toolbar.
update:openvalue: booleanEmitted with false alongside close.

Slots ​

SlotPropsDescription
default(none)The controls, usually NbButtons.

Exposed methods ​

MemberTypeDescription
reposition() => voidRe-reads the anchor and places the toolbar. Call on selectionchange.
focus() => voidMoves keyboard focus onto the toolbar's current tab stop.
elRef<HTMLElement | null>The toolbar element while it is rendered.

Tokens used ​

TokenApplied to
--nb-c-surface, --nb-c-borderToolbar surface and border (overlay layer)
--nb-c-scrimShadow
--nb-radius-* (popover surface)Corner rounding
--nb-zindex-menuStacking
--nb-base-unitPadding and gap