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' ;
2. Subpath Imports (Recommended for Bundle Size)
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
Subpath Exports @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.5 rem ;
}
.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:
CSS variables — Override the tokens above in :root or .dark for global theme.
ClassName — Pass className to component parts to target specific elements.
Inline styles — For one-off overrides.
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