Skip to main content

Overview

FloatingToolbar is a self-contained component that displays a formatting toolbar when text is selected. It handles selection tracking internally and exposes visibility state via context.
Floating toolbar screenshot

Features

  • API — compound components with auto-visibility
  • Selection tracking — automatically shows/hides on text selection
  • Block selection support — also works when blocks are selected
  • frozen prop — pause selection tracking when popups are open
  • TypeScript — full type safety

Installation

npm install @yoopta/ui
# or
yarn add @yoopta/ui

Basic Usage

import { FloatingToolbar } from '@yoopta/ui/floating-toolbar';
import { Marks, useYooptaEditor } from '@yoopta/editor';
import { FontBoldIcon, FontItalicIcon, CodeIcon } from '@radix-ui/react-icons';

function MyToolbar() {
  const editor = useYooptaEditor();

  return (
    <FloatingToolbar>
      <FloatingToolbar.Content>
        <FloatingToolbar.Group>
          {editor.formats.bold && (
            <FloatingToolbar.Button
              onClick={() => Marks.toggle(editor, { type: 'bold' })}
              active={Marks.isActive(editor, { type: 'bold' })}
              title="Bold">
              <FontBoldIcon />
            </FloatingToolbar.Button>
          )}
          {editor.formats.italic && (
            <FloatingToolbar.Button
              onClick={() => Marks.toggle(editor, { type: 'italic' })}
              active={Marks.isActive(editor, { type: 'italic' })}
              title="Italic">
              <FontItalicIcon />
            </FloatingToolbar.Button>
          )}
          {editor.formats.code && (
            <FloatingToolbar.Button
              onClick={() => Marks.toggle(editor, { type: 'code' })}
              active={Marks.isActive(editor, { type: 'code' })}
              title="Code">
              <CodeIcon />
            </FloatingToolbar.Button>
          )}
        </FloatingToolbar.Group>
      </FloatingToolbar.Content>
    </FloatingToolbar>
  );
}

// In your editor: create editor with createYooptaEditor({ plugins, marks });
// UI components as children
<YooptaEditor editor={editor} onChange={(value) => console.log(value)} placeholder="Type / to open menu...">
  <MyToolbar />
</YooptaEditor>

API Reference

FloatingToolbar (Root)

Root component that handles selection tracking.
<FloatingToolbar frozen={popoverOpen}>
  {/* or with render props */}
  {({ isOpen }) => isOpen && <FloatingToolbar.Content>...</FloatingToolbar.Content>}
</FloatingToolbar>
Props:
PropTypeDescription
childrenReactNode | ((api) => ReactNode)Content or render function
frozenbooleanWhen true, selection tracking is paused
classNamestringCustom CSS classes
Render Props API:
PropertyTypeDescription
isOpenbooleanWhether toolbar is visible

FloatingToolbar.Content

Floating content panel. Automatically hides when no selection.
<FloatingToolbar.Content className="custom-class">
  {/* Groups and buttons */}
</FloatingToolbar.Content>
Props:
PropTypeDescription
classNamestringCustom CSS classes
childrenReactNodeToolbar groups and buttons

FloatingToolbar.Group

Groups related buttons together.
<FloatingToolbar.Group>
  <FloatingToolbar.Button>Bold</FloatingToolbar.Button>
  <FloatingToolbar.Button>Italic</FloatingToolbar.Button>
</FloatingToolbar.Group>

FloatingToolbar.Button

Individual toolbar button.
<FloatingToolbar.Button onClick={handleClick} active={isActive} disabled={false} title="Bold">
  <BoldIcon />
</FloatingToolbar.Button>
Props:
PropTypeDescription
onClick(event) => voidClick handler
activebooleanWhether button is active/pressed
disabledbooleanDisable the button
titlestringTooltip text
classNamestringCustom CSS classes

FloatingToolbar.Separator

Visual separator between groups.
<FloatingToolbar.Separator />

Examples

With Turn Into Button

import { useRef, useState } from 'react';
import { Blocks, useYooptaEditor } from '@yoopta/editor';
import { FloatingToolbar } from '@yoopta/ui/floating-toolbar';
import { ActionMenuList } from '@yoopta/ui/action-menu-list';

function MyToolbar() {
  const editor = useYooptaEditor();
  const turnIntoRef = useRef<HTMLButtonElement>(null);
  const [actionMenuOpen, setActionMenuOpen] = useState(false);

  const currentBlockId =
    typeof editor.path.current === 'number'
      ? Blocks.getBlock(editor, { at: editor.path.current })?.id ?? null
      : null;

  return (
    <>
      <FloatingToolbar frozen={actionMenuOpen}>
        <FloatingToolbar.Content>
          <FloatingToolbar.Group>
            <FloatingToolbar.Button ref={turnIntoRef} onClick={() => setActionMenuOpen(true)}>
              Turn into
              <ChevronDownIcon />
            </FloatingToolbar.Button>
          </FloatingToolbar.Group>
          <FloatingToolbar.Separator />
          <FloatingToolbar.Group>{/* Formatting buttons */}</FloatingToolbar.Group>
        </FloatingToolbar.Content>
      </FloatingToolbar>

      <ActionMenuList
        open={actionMenuOpen}
        onOpenChange={setActionMenuOpen}
        anchor={turnIntoRef.current}
        blockId={currentBlockId}
        view="small"
        placement="bottom-start">
        <ActionMenuList.Content />
      </ActionMenuList>
    </>
  );
}

Rich Formatting Toolbar

function FormattingToolbar() {
  const editor = useYooptaEditor();

  const buttons = [
    { type: 'bold', icon: <BoldIcon />, label: 'Bold' },
    { type: 'italic', icon: <ItalicIcon />, label: 'Italic' },
    { type: 'underline', icon: <UnderlineIcon />, label: 'Underline' },
    { type: 'strike', icon: <StrikethroughIcon />, label: 'Strikethrough' },
    { type: 'code', icon: <CodeIcon />, label: 'Code' },
  ];

  return (
    <FloatingToolbar>
      <FloatingToolbar.Content>
        <FloatingToolbar.Group>
          {buttons.map((btn) => {
            const isActive = Marks.isActive(editor, { type: btn.type });
            if (!editor.formats[btn.type]) return null;

            return (
              <FloatingToolbar.Button
                key={btn.type}
                onClick={() => Marks.toggle(editor, { type: btn.type })}
                active={isActive}
                title={btn.label}>
                {btn.icon}
              </FloatingToolbar.Button>
            );
          })}
        </FloatingToolbar.Group>
      </FloatingToolbar.Content>
    </FloatingToolbar>
  );
}

Styling

CSS Variables

:root {
  --yoopta-ui-floating-toolbar-gap: 4px;
  --yoopta-ui-floating-toolbar-padding: 6px;
  --yoopta-ui-floating-toolbar-radius: 0.5rem;
  --yoopta-ui-floating-toolbar-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --yoopta-ui-floating-toolbar-button-min-width: 32px;
  --yoopta-ui-floating-toolbar-button-min-height: 32px;
  --yoopta-ui-floating-toolbar-button-hover: var(--yoopta-ui-accent);
}

Custom Styles

<FloatingToolbar>
  <FloatingToolbar.Content className="bg-slate-900 border-white/10">
    <FloatingToolbar.Group>
      <FloatingToolbar.Button className="text-white hover:bg-white/10">Bold</FloatingToolbar.Button>
    </FloatingToolbar.Group>
  </FloatingToolbar.Content>
</FloatingToolbar>

Best Practices

const [popoverOpen, setPopoverOpen] = useState(false);

<FloatingToolbar frozen={popoverOpen}>
  {/* Prevents toolbar from closing while popover is open */}
</FloatingToolbar>
{editor.formats.bold && (
  <FloatingToolbar.Button onClick={() => Marks.toggle(editor, { type: 'bold' })}>
    Bold
  </FloatingToolbar.Button>
)}