---
title: "Block UI (drag handle, hover actions)"
description: "Add the drag handle, hover targeting, block actions and block-range selection with blockUiFeature()."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/block-ui"
---
# Block UI (drag handle, hover actions)

> Add the drag handle, hover targeting, block actions and block-range selection with blockUiFeature().

## Adding block UI

```ts
import { blockUiFeature } from '@skaile/forge-editor/block-ui/vue' // or '@skaile/forge-editor/block-ui/react'
const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), blockUiFeature()] })
```

`blockUiFeature()` adds the drag handle on the hovered block's left, hover targeting, the `BlockActionsOverlay` (the Block actions of the features and of the host) on the block's top line at the editor's right edge, and block-range selection (`Mod`+drag, `Shift`+`↑`/`↓`). It is opt-in: the simple editor never includes it. The drag handle's upstream package imports `@tiptap/extension-collaboration` and `y-tiptap`, so a preset with block UI bundles that support code even without a live session.

## Where block actions appear

`blockUiFeature({ actions })` (or `composeEditor({ …, blockUi: { actions } })`; `BlockUiOptions`) chooses the surfaces that render block actions:

<table>
<thead>
  <tr>
    <th>
      <code>
        actions
      </code>
    </th>
    
    <th>
      Surface
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'hover'
      </code>
      
       (default)
    </td>
    
    <td>
      the bar of the block under the pointer, at the editor's right edge
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'toolbar'
      </code>
    </td>
    
    <td>
      the bar of the block holding the selection (a caret in the block, a node selection), shown at the block's corner as long as the selection stays in it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'both'
      </code>
    </td>
    
    <td>
      both; where the pointer hovers the selected block only the toolbar shows, so a block never has two bars
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'none'
      </code>
    </td>
    
    <td>
      no action bar; the drag handle, targeting and block-range selection stay
    </td>
  </tr>
</tbody>
</table>

The Block UI addresses the **outermost block** below the nearest block container (the document or a Typed block's body), resolved by `outerBlockAt($pos)` / `outerBlocksAt($pos)` from `@skaile/forge-editor/block-ui`. A list item or a quote paragraph is not a Block of its own: with the caret or the pointer in one, the hover bar, the toolbar actions and the drag handle (hover and permanent) address the whole list, nested lists included, or the whole quote. A paragraph in a Typed block's body is its own block, a Typed block's chrome (title, description, tags) stands for the Typed block, and a Typed block inside a Typed block is a block of its own. Block IDs are unchanged: nested paragraphs keep theirs, so AI proposals and comments on them still work.

The toolbar bar is the same `BlockActionsOverlay` with `surface="toolbar"` and `persistent`; its root carries the class `block-actions-toolbar` next to `block-actions`, so a host can style it apart from the hover bar. A caret in a Typed block's title, description or tags row addresses the Typed block; a selection across several top-level blocks has no toolbar. While the editor is read-only, both bars offer only the actions marked `readOnly` (see below).

Each Block action may narrow itself with `placement: 'hover' | 'toolbar' | 'both'` (default `'both'`); the built-in actions (convert, duplicate, delete, AI, Typed block summary) keep the default. Abilities are separate: an ability (`ABILITY`, such as `block.delete`) stays a permission ("can"), so removing `block.delete` hides the action on every surface, while `actions` and `placement` only choose where the allowed actions are offered. Hosts that used to strip `block.convert`, `block.duplicate` and `block.delete` abilities only to empty the hover bar can pass `actions: 'none'` (or `'toolbar'`) instead and keep the abilities, so `convertMenuItems` and the ⌘K palette are unaffected.

The toolbar addresses the innermost block, so with the caret in a paragraph of a Typed block's body the Typed block's own actions (split, merge, export, focus) would not be offered. An action that belongs to the Typed block declares `target: 'typedBlock'` (default `'block'`): on the toolbar it is then checked against and runs with the nearest Typed block around the selected block, or the block itself when that is a Typed block (`typedBlockAround(editor, target)` from `@skaile/forge-editor/block-ui`). `ability`, `requires`, `blockTypes` and `isVisible` see that Typed block, and `ctx.node` and `ctx.pos` are its own; with no Typed block around (a paragraph at the document root) the action is absent. With nested Typed blocks the innermost one wins. The bar still docks at the selected block's corner. The hover bar ignores `target`: hovering a Typed block's chrome already addresses the Typed block.

## Actions in a read-only editor

A Block action is hidden while the editor is read-only unless it sets `readOnly: true` ("offered in a read-only editor too", default `false`). `blockActionsFor(editor, target, actions, surface)` applies the rule, so the hover and the toolbar bar behave the same in the Vue and React overlays; the bar shows as soon as one action remains. Mark only actions that do not change the document: the built-in `composite-details` action („Details“, the info icon, hover bar) is the first one and opens the details popover of the Typed block, which renders read-only in such an editor. The drag handle stays hidden. `selectedBlockOf` still resolves the selected block in a read-only editor; an action that mutates is filtered out by the missing `readOnly` flag, so it needs no `isEditable` check of its own.

An action may also set `onHover(ctx, entering)`: the overlay calls it with `true` when the pointer enters the action's button and with `false` when it leaves, for example to highlight the block the action is about. It gets the same `ctx` as `run` (`BlockActionContext`) and works for `run` and `menu` actions alike.

## Handles per block

Which blocks have which handle follows the settings and the block:

- **Blocks with a Block type icon** (Typed blocks, see [Typed blocks](https://skaile-ai.github.io/forge-editor/guides/composites)) have no standard handle: their icon is the handle. Dragging it selects the Typed block and moves it like the standard handle does; a click without movement opens the details popover.
- **blocks.dragHandle: 'hover'** (default): one handle follows the hovered block.
- **blocks.dragHandle: 'always'**: the hover handle is replaced by one permanent handle per eligible block, each at the position the hover handle would take for that block (right edge on the block's left edge, centred on its first text line). Eligible are the blocks the hover handle can target (see below) at any depth. Not eligible: Typed block chrome (title, description, tags), everything inside a list, quote or table (the list, quote or table is the block), and blocks with a Block type icon. A nested block whose handle would cover its container's handle on the same line has none; the container's handle moves it. Only blocks within one screen of the viewport render a handle. Both modes are hidden while the editor is read-only.

## Blocks with icons of their own in the top-right corner

The hover bar sits at the editor's right edge, which a node view that draws its own icons there (status badges, a Block type or properties button) would have covered. Such a block marks those elements with the attribute `data-block-corner` (`BLOCK_CORNER_ATTR`, `blockCornerOf(dom)` from `@skaile/forge-editor/block-ui`): the bar docks immediately left of the leftmost marked element (4px gap) and is centred on the first one's row, so bar and icons read as one row. Marked elements inside a nested block's own node view (`[data-node-view-wrapper]`) belong to that block, hidden ones (no layout box) are ignored, and a block without marked elements keeps the placement at the editor's right edge. The overlay re-measures when a marked element resizes; Vue and React behave the same.

## Dragging from outside the editor

`startBlockDrag(editor, event, pos)` (`@skaile/forge-editor/core`) starts such a drag from any element outside the editor's DOM: call it from the element's `dragstart`.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
