Floating button group

Separate soft buttons in a quiet tray, with one shared highlight that morphs from button to button as you move, and a pressed state that settles in place.

actiongrouptoolbarnew
Preview
Floating button group preview

When to use

  • A floating editor or canvas toolbar with undo, redo, share and present.
  • A selection toolbar that appears over text or objects.
  • A vertical tools rail with zoom and insert actions.

When not to use

  • Use toggle-group for formatting toggles or alignment where the choice stays pressed.
  • Use segmented-control to switch between views.
  • Use split-button when there is one main action with a few alternatives.
  • Use a dropdown menu when there are more actions than fit in one row.

shadcn CLI

Also installs (2)

  • manicat-foundation
  • tooltip
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-button-group",
  "type": "registry:ui",
  "title": "Floating button group",
  "description": "Separate soft buttons in a quiet tray, with one shared highlight that morphs from button to button as you move, and a pressed state that settles in place.",
  "categories": [
    "actions"
  ],
  "dependencies": [
    "@radix-ui/react-tooltip",
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json",
    "https://manicat.viraza.net/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/components/floating-button-group/floating-button-group.tsx",
      "type": "registry:component",
      "target": "@components/manicat/floating-button-group/floating-button-group.tsx"
    },
    {
      "path": "registry/components/floating-button-group/floating-button-group.module.css",
      "type": "registry:component",
      "target": "@components/manicat/floating-button-group/floating-button-group.module.css"
    },
    {
      "path": "registry/components/tooltip/tooltip.tsx",
      "type": "registry:component",
      "target": "@components/manicat/tooltip/tooltip.tsx"
    },
    {
      "path": "registry/components/tooltip/tooltip.module.css",
      "type": "registry:component",
      "target": "@components/manicat/tooltip/tooltip.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/floating-button-group",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/floating-button-group",
    "markdown": "https://uiarc.dev/components/floating-button-group/markdown",
    "tags": [
      "action",
      "group",
      "toolbar",
      "new"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { MoreHorizontal, Share2, Star, Trash2, Undo2 } from "lucide-react";
import { FloatingButtonGroup } from "@/components/manicat/floating-button-group/floating-button-group";

export function FloatingButtonGroupExample() {
  return (
    <FloatingButtonGroup
      label="Board actions"
      items={[
        { id: "select", label: "Select" },
        { id: "draw", label: "Draw" },
        { id: "share", label: "Share" },
        { type: "separator" },
        { id: "undo", label: "Undo", icon: <Undo2 size={16} />, shortcut: "⌘Z" },
      ]}
    />
  );
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Whiteboard tools A floating toolbar with a separator and a shortcut hint on the last tool.

Whiteboard tools A floating toolbar with a separator and a shortcut hint on the last tool. preview

Keyboard interactions

KeysAction
TabMoves into the toolbar on one button, the last one focused, and out again. The highlight appears on the focused button.
Arrow left / Arrow rightIn a row: moves focus to the previous or next enabled button, wrapping at the ends. The highlight follows.
Arrow up / Arrow downIn a column: moves focus to the previous or next enabled button, wrapping at the ends.
Home / EndMoves focus to the first or last enabled button.
Enter / SpaceRuns the focused action. The highlight darkens while the key is held.

Accessibility

  • The tray is a toolbar with aria-label and aria-orientation, and a single tab stop that roves between buttons.
  • Every button has an accessible name from its label, also when it shows only an icon. Icon only buttons and buttons with a shortcut get a tooltip with the label and shortcut, which screen readers hear as the description.
  • Mode actions set aria-pressed. Disabled actions use aria-disabled, stay readable with their tooltip, and are skipped by the arrow keys.
  • Separators have the separator role with the orientation across the toolbar.
  • Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The shared highlight is the keyboard indicator, shown only for keyboard focus so a mouse click never leaves it behind.
  • When a label changes, such as Share to Copied, the accessible name changes with it and the outgoing label is hidden from assistive technology while it fades. Reserved label ghosts are hidden from assistive technology.

Motion

  • One highlight per tray, driven by motion values so moving it never re-renders the buttons. It travels in position and size to the hovered or focused button on the morph spring, and stays put while the pointer crosses a gap or a separator.
  • Appearing from nothing it lands in place and grows in from 94 percent on the snappy spring, instead of sliding from wherever it last was. It fades when the pointer leaves the tray.
  • Pressing darkens the highlight and settles it to 96.5 percent in place on the snappy spring; the buttons themselves never scale, so neighbours and tooltip anchors stay still.
  • A new label crossfades in place with a short rise and blur. With reserveLabels the button already has the widest label's width, so nothing around it moves; without it, the highlight re-measures and follows the new width.
  • Nothing but the highlight ever moves: hover, focus, press and label changes leave the tray and every button at the same box. The highlight is measured exactly in tray coordinates, snapped to device pixels and corrected for ancestor transforms.
  • The pointer leaving the tray is detected with a native listener, because React bubbles pointer events through portals and a tooltip would otherwise count as inside the tray.
  • With reduced motion the highlight jumps to its button and fades briefly, and labels crossfade without travel.

Responsive behavior

  • The tray keeps its natural size and never wraps or clips a button. At narrow widths make secondary items icon only; to switch without a layout shift after hydration, render both densities and pick one with a container query, as the demo does below 360px.
  • Touch shows the highlight under the finger while pressed and fades it on release; hover only tracks a mouse or pen.
  • The highlight re-measures when the tray or any button resizes, so it stays aligned through font loading, label changes and container reflow.

Performance

  • The highlight moves on transform and size through motion values without React renders; only hover and focus changes re-render the buttons.
  • One ResizeObserver per tray, observing the tray and its buttons. Reserved labels are plain CSS grid ghosts, measured by layout, not by script.

Notes for AI

  • Use it for actions, not selection. When one or more options stay chosen, use toggle-group or segmented-control.
  • Group related actions with separators and keep a toolbar to about seven buttons. Make secondary actions icon only before dropping labels from the main one.
  • Give every icon only item a specific label, such as "Zoom to fit", not "Fit".
  • shortcut is a hint only. Bind the key yourself, scoped so it does not fight the page.
  • Pass reserveLabels whenever an item's label changes, so the toolbar never shifts.
  • Use variant floating when the toolbar sits over content such as a canvas or a text selection; use muted in a page or a panel.