Skip to main content

Introduction

The @yoopta/ui package provides a collection of headless UI components that give you complete control over your editor’s interface. These components follow modern design patterns, offering flexibility and customization while maintaining excellent developer experience.
Starting with Yoopta Editor v6, the core @yoopta/editor is completely headless. All UI components use the compound component pattern with no shared global state.

Key Features

API

Compound components with controlled/uncontrolled patterns

TypeScript Support

Full TypeScript support with exported types for every component

Accessibility

Built with accessibility in mind, following ARIA best practices

Installation

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

Import Styles

Two ways to import the @yoopta/ui package:

1. Full Package Import

Import everything from the main entry point:
import {
  FloatingBlockActions,
  BlockOptions,
  FloatingToolbar,
  ActionMenuList,
  SlashCommandMenu,
  SelectionBox,
  BlockDndContext,
  SortableBlock,
  DragHandle,
  HighlightColorPicker,
  ElementOptions,
  Portal,
  Overlay,
} from '@yoopta/ui';
Import only what you need using subpath exports for better tree-shaking and smaller bundles:
// Only import the floating toolbar
import { FloatingToolbar } from '@yoopta/ui/floating-toolbar';

// Only import action menu list
import { ActionMenuList } from '@yoopta/ui/action-menu-list';

// Only import slash command menu
import { SlashCommandMenu } from '@yoopta/ui/slash-command-menu';

// Other available subpaths
import { FloatingBlockActions } from '@yoopta/ui/floating-block-actions';
import { BlockOptions, useBlockActions, useBlockOptionsContext } from '@yoopta/ui/block-options';
import { SelectionBox, useRectangeSelectionBox } from '@yoopta/ui/selection-box';
import {
  BlockDndContext,
  SortableBlock,
  DragHandle,
  useBlockDnd,
  getOrderedBlockIds,
} from '@yoopta/ui/block-dnd';
import { HighlightColorPicker } from '@yoopta/ui/highlight-color-picker';
import {
  ElementOptions,
  useElementOptions,
  useUpdateElementProps,
} from '@yoopta/ui/element-options';
import { Portal } from '@yoopta/ui/portal';
import { Overlay } from '@yoopta/ui/overlay';
When to use subpath imports:
  • You only need 1-2 components
  • Bundle size is critical (e.g., landing pages)
  • You want explicit control over what’s included
When to use full import:
  • You use most components
  • Convenience over optimization
  • Your bundler has good tree-shaking

Available Subpaths

SubpathExports
@yoopta/uiEverything (convenience)
@yoopta/ui/floating-block-actionsFloatingBlockActions
@yoopta/ui/block-optionsBlockOptions, useBlockActions, useBlockOptionsContext
@yoopta/ui/floating-toolbarFloatingToolbar
@yoopta/ui/action-menu-listActionMenuList
@yoopta/ui/slash-command-menuSlashCommandMenu
@yoopta/ui/selection-boxSelectionBox, useRectangeSelectionBox
@yoopta/ui/block-dndBlockDndContext, SortableBlock, DragHandle, useBlockDnd, getOrderedBlockIds
@yoopta/ui/highlight-color-pickerHighlightColorPicker
@yoopta/ui/element-optionsElementOptions, useElementOptions, useElementOptionsContext, useUpdateElementProps + types
@yoopta/ui/portalPortal
@yoopta/ui/overlayOverlay

Available Components

All components from @yoopta/ui are listed below. Dedicated doc pages exist for: FloatingBlockActions, BlockOptions, FloatingToolbar, ActionMenuList, SlashCommandMenu, SelectionBox, and Block DnD. HighlightColorPicker and ElementOptions are exported and usable but do not have dedicated pages yet.

Core UI Components

Utilities: Portal and Overlay are also exported for rendering outside the editor tree (subpaths: @yoopta/ui/portal, @yoopta/ui/overlay).

Architecture

Compound Components

All components follow the compound component pattern:
<FloatingToolbar frozen={popoverOpen}>
  <FloatingToolbar.Content>
    <FloatingToolbar.Group>
      <FloatingToolbar.Button onClick={handleBold}>
        <BoldIcon />
      </FloatingToolbar.Button>
    </FloatingToolbar.Group>
  </FloatingToolbar.Content>
</FloatingToolbar>

Controlled vs Self-Managed

Components fall into two categories:

1. Self-Managed Components

These handle their own visibility automatically:
  • FloatingBlockActions - Appears on block hover
  • FloatingToolbar - Appears on text selection
  • SlashCommandMenu - Appears on / command
// Just render them - they handle visibility automatically
<YooptaEditor editor={editor}>
  <FloatingBlockActions>
    {({ blockId }) => (
      <FloatingBlockActions.Button onClick={() => onPlusClick(blockId)}>
        <PlusIcon />
      </FloatingBlockActions.Button>
    )}
  </FloatingBlockActions>
</YooptaEditor>

2. Controlled Components

These require explicit open/close control:
  • BlockOptions - Controlled via open/onOpenChange
  • ActionMenuList - Controlled via open/onOpenChange
const [open, setOpen] = useState(false);

<BlockOptions open={open} onOpenChange={setOpen} anchor={buttonRef.current}>
  <BlockOptions.Content>{/* Items */}</BlockOptions.Content>
</BlockOptions>;

The frozen Prop Pattern

When opening submenus, use the frozen prop to pause the parent’s tracking:
const [blockOptionsOpen, setBlockOptionsOpen] = useState(false);

<FloatingBlockActions frozen={blockOptionsOpen}>
  {({ blockId }) => (
    <>
      <FloatingBlockActions.Button onClick={() => setBlockOptionsOpen(true)}>
        <DragHandleIcon />
      </FloatingBlockActions.Button>

      <BlockOptions open={blockOptionsOpen} onOpenChange={setBlockOptionsOpen} blockId={blockId} />
    </>
  )}
</FloatingBlockActions>;

Quick Start

Here’s a complete example using the UI components:
import { useMemo, useState, useRef } from 'react';
import { createYooptaEditor, YooptaEditor, Blocks, Marks, useYooptaEditor } from '@yoopta/editor';
import Paragraph from '@yoopta/paragraph';
import { Bold, Italic } from '@yoopta/marks';
import { FloatingBlockActions } from '@yoopta/ui/floating-block-actions';
import { BlockOptions, useBlockActions } from '@yoopta/ui/block-options';
import { FloatingToolbar } from '@yoopta/ui/floating-toolbar';
import { ActionMenuList } from '@yoopta/ui/action-menu-list';
import { SlashCommandMenu } from '@yoopta/ui/slash-command-menu';

const plugins = [Paragraph];
const marks = [Bold, Italic];

// FloatingBlockActions with BlockOptions
function MyFloatingBlockActions() {
  const editor = useYooptaEditor();
  const [blockOptionsOpen, setBlockOptionsOpen] = useState(false);
  const dragHandleRef = useRef<HTMLButtonElement>(null);

  const onPlusClick = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    editor.insertBlock('Paragraph', { at: block.meta.order + 1, focus: true });
  };

  return (
    <FloatingBlockActions frozen={blockOptionsOpen}>
      {({ blockId }) => (
        <>
          <FloatingBlockActions.Button onClick={() => onPlusClick(blockId)}>
            <PlusIcon />
          </FloatingBlockActions.Button>
          <FloatingBlockActions.Button
            ref={dragHandleRef}
            onClick={() => setBlockOptionsOpen(true)}>
            <DragHandleIcon />
          </FloatingBlockActions.Button>

          <MyBlockOptions
            open={blockOptionsOpen}
            onOpenChange={setBlockOptionsOpen}
            blockId={blockId}
            anchor={dragHandleRef.current}
          />
        </>
      )}
    </FloatingBlockActions>
  );
}

// BlockOptions with ActionMenuList
function MyBlockOptions({ open, onOpenChange, blockId, anchor }) {
  const { duplicateBlock, deleteBlock } = useBlockActions();
  const turnIntoRef = useRef<HTMLButtonElement>(null);
  const [actionMenuOpen, setActionMenuOpen] = useState(false);

  const onActionMenuClose = (menuOpen: boolean) => {
    setActionMenuOpen(menuOpen);
    if (!menuOpen) onOpenChange?.(false);
  };

  return (
    <>
      <BlockOptions open={open} onOpenChange={onOpenChange} anchor={anchor}>
        <BlockOptions.Content side="right">
          <BlockOptions.Group>
            <BlockOptions.Item ref={turnIntoRef} onSelect={() => setActionMenuOpen(true)} keepOpen>
              Turn into
            </BlockOptions.Item>
          </BlockOptions.Group>
          <BlockOptions.Separator />
          <BlockOptions.Group>
            <BlockOptions.Item onSelect={() => duplicateBlock(blockId)}>
              Duplicate
            </BlockOptions.Item>
            <BlockOptions.Item variant="destructive" onSelect={() => deleteBlock(blockId)}>
              Delete
            </BlockOptions.Item>
          </BlockOptions.Group>
        </BlockOptions.Content>
      </BlockOptions>

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

// FloatingToolbar
function MyFloatingToolbar() {
  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' })}>
              <BoldIcon />
            </FloatingToolbar.Button>
          )}
          {editor.formats.italic && (
            <FloatingToolbar.Button
              onClick={() => Marks.toggle(editor, { type: 'italic' })}
              active={Marks.isActive(editor, { type: 'italic' })}>
              <ItalicIcon />
            </FloatingToolbar.Button>
          )}
        </FloatingToolbar.Group>
      </FloatingToolbar.Content>
    </FloatingToolbar>
  );
}

// Main Editor: create editor with plugins/marks; UI components as children
function App() {
  const editor = useMemo(() => createYooptaEditor({ plugins, marks }), []);

  return (
    <YooptaEditor
      editor={editor}
      onChange={(value) => console.log(value)}
      placeholder="Type / to open menu...">
      <MyFloatingBlockActions />
      <MyFloatingToolbar />
      <SlashCommandMenu />
    </YooptaEditor>
  );
}
Plugins and marks are passed to createYooptaEditor, not to YooptaEditor. UI components must be children of YooptaEditor so they can use useYooptaEditor() for the editor instance.

Styling

How styles work

  • Each UI component ships with its own CSS. Component styles use a shared set of design tokens (CSS variables) defined in the package (packages/core/ui/src/styles/variables.css).
  • Each component’s CSS file imports this variables file via @import '../styles/variables.css'. At build time, postcss-import inlines the variables into the component CSS, so default styling works without any extra imports in your app.
  • You do not need to import variables.css yourself for the components to look correct. Import a component from @yoopta/ui (or a subpath), and its styles (including the inlined variables) are applied.

Theming with CSS variables

All components use the same token names (shadcn/ui-style HSL values). Override them in your app to theme:
/* In your app's global CSS */
:root {
  --yoopta-ui-background: 0 0% 100%;
  --yoopta-ui-foreground: 222.2 84% 4.9%;
  --yoopta-ui-border: 214.3 31.8% 91.4%;
  --yoopta-ui-accent: 210 40% 96.1%;
  --yoopta-ui-muted-foreground: 215.4 16.3% 46.9%;
  --yoopta-ui-ring: 222.2 84% 4.9%;
  --yoopta-ui-primary: 221.2 83.2% 53.3%;
  --yoopta-ui-radius: 0.5rem;
}

.dark,
[data-theme='dark'],
[data-yoopta-theme='dark'] {
  --yoopta-ui-background: 222.2 84% 4.9%;
  --yoopta-ui-foreground: 210 40% 98%;
  --yoopta-ui-border: 217.2 32.6% 17.5%;
  --yoopta-ui-accent: 217.2 32.6% 17.5%;
}
Values are HSL without the hsl() wrapper; components use them as hsl(var(--yoopta-ui-background)).

Custom styles

You can still override appearance with:
  1. CSS variables — Override the tokens above in :root or .dark for global theme.
  2. ClassName — Pass className to component parts to target specific elements.
  3. Inline styles — For one-off overrides.
  4. Tailwind — Use utility classes on the same elements.
<FloatingToolbar.Content className="bg-slate-800 shadow-xl">
  <FloatingToolbar.Button className="text-white hover:bg-white/10">Bold</FloatingToolbar.Button>
</FloatingToolbar.Content>

TypeScript Support

Full TypeScript support with exported types:
import type {
  FloatingBlockActionsProps,
  BlockOptionsProps,
  ActionMenuListProps,
  ActionMenuItem,
} from '@yoopta/ui';

Migration from v4.9

If you’re migrating from the old built-in UI:
The old ActionMenuTool, Toolbar, and LinkTool from @yoopta/tools are deprecated. Use the new components from @yoopta/ui instead.
Before (v4.9):
import ActionMenuList from '@yoopta/action-menu';
import Toolbar from '@yoopta/toolbar';

<YooptaEditor tools={[ActionMenuList, Toolbar]} />;
After (v6):
import { SlashCommandMenu, FloatingToolbar } from '@yoopta/ui';

const editor = useMemo(() => createYooptaEditor({ plugins, marks }), []);

<YooptaEditor editor={editor} onChange={onChange} placeholder="Type / to open menu...">
  <SlashCommandMenu />
  <MyFloatingToolbar />
</YooptaEditor>;

Next Steps