Block Handle
Gutter control cluster rendered beside an editor block. Provides an insert-below shortcut and a drag handle that opens the block menu (turn-into-menu's block-type list plus move, duplicate and delete). Composed as <x-ui.editor.block-handle>; <x-ui.block-handle> stays as the compatibility tag.
Preview
Hover a block
The handle appears in the gutter beside the block under the pointer: the plus adds a paragraph below it, the grip opens the block menu (turn into, move, duplicate, delete).
Press and drag the grip to move a block; the drop indicator marks the gap it will land in.
- First item
- Second item
Insert image
Describe the image for people who cannot see it.
Uploading image…
Adding page…
<x-ui.editor class="mx-auto max-w-xl" placeholder="Write something…">
<x-slot:tools>
<x-ui.editor.block-handle />
<x-ui.editor.drop-indicator />
</x-slot:tools>
<h2>Hover a block</h2>
<p>The handle appears in the gutter beside the block under the pointer: the plus adds a paragraph below it, the grip opens the block menu (turn into, move, duplicate, delete).</p>
<p>Press and drag the grip to move a block; the drop indicator marks the gap it will land in.</p>
<ul><li>First item</li><li>Second item</li></ul>
</x-ui.editor>
Installation
php artisan ui:add block-handle
Note
This component ships an Alpine behavior module at
resources/js/ui/block-handle.js. Import it once from your bundle so it registers on alpine:init:
import './block-handle.js';
Registry contract
php artisan ui:add block-handle
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/editor/block-handle.blade.php -
resources/views/components/ui/block-handle.blade.php
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Block Handle (`block-handle`)
Gutter control cluster rendered beside an editor block. Provides an insert-below shortcut and a drag handle that opens the block menu (turn-into-menu's block-type list plus move, duplicate and delete). Composed as <x-ui.editor.block-handle>; <x-ui.block-handle> stays as the compatibility tag.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add block-handle
```
## Usage
```blade
<x-ui.editor class="mx-auto max-w-xl" placeholder="Write something…">
<x-slot:tools>
<x-ui.editor.block-handle />
<x-ui.editor.drop-indicator />
</x-slot:tools>
<h2>Hover a block</h2>
<p>The handle appears in the gutter beside the block under the pointer: the plus adds a paragraph below it, the grip opens the block menu (turn into, move, duplicate, delete).</p>
<p>Press and drag the grip to move a block; the drop indicator marks the gap it will land in.</p>
<ul><li>First item</li><li>Second item</li></ul>
</x-ui.editor>
```
## Use when
- Use when users need rich structured authoring, annotation, or collaborative editing controls.
- Giving each block in the editor a gutter control for inserting, reordering by drag, or opening the block menu (turn into, move, duplicate, delete).
## Avoid when
- Do not use rich editor affordances for simple one-line or plain-text tasks.
- The host is not a contenteditable editor surface; block-level drag-and-drop belongs to sortable instead.
## Anti-patterns
- Rich editors for one-line or easily validated input
## Rules
- Use the `<brok:block-handle>` tag (or `<x-ui.block-handle>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/block-handle
- Registry JSON (files, props, contract): https://brokui.dev/r/open/block-handle.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
No documented props. Pass normal Blade attributes through the component root.
Slots
default—x-ui.editor.block-handle— Installed subcomponent from the registry item.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Stamps data-gutter on the nearest ancestor editor root, which opens a start gutter for it, and follows the hovered block through the editor's shared handle state.
- The grip's click opens turn-into-menu's block menu; a press-and-drag instead reorders the block, with drop-indicator marking the target gap.
- Standalone, outside an editor, it renders inline and its dispatched events bubble to nothing, so it stays harmless.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Create or revise structured rich content.
Use when
- Use when users need rich structured authoring, annotation, or collaborative editing controls.
- Giving each block in the editor a gutter control for inserting, reordering by drag, or opening the block menu (turn into, move, duplicate, delete).
Avoid when
- Do not use rich editor affordances for simple one-line or plain-text tasks.
- The host is not a contenteditable editor surface; block-level drag-and-drop belongs to sortable instead.
Use instead
- Textarea or plain field for simple content
Anti-patterns
- Rich editors for one-line or easily validated input
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- native
- Focus
native
- Every control uses @mousedown.prevent so clicking the handle never blurs the contenteditable region and drops the current selection.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="block-handle-{{ $record->id }}">
<x-ui.editor class="mx-auto max-w-xl" placeholder="Write something…">
<x-slot:tools>
<x-ui.editor.block-handle />
<x-ui.editor.drop-indicator />
</x-slot:tools>
<h2>Hover a block</h2>
<p>The handle appears in the gutter beside the block under the pointer: the plus adds a paragraph below it, the grip opens the block menu (turn into, move, duplicate, delete).</p>
<p>Press and drag the grip to move a block; the drop indicator marks the gap it will land in.</p>
<ul><li>First item</li><li>Second item</li></ul>
</x-ui.editor>
</div>
Validation
Validation support: native. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:block-handle
wire:model="value"
:aria-invalid="$errors->has('value') ? 'true' : 'false'"
aria-describedby="value-error"
/>
@error('value')
<p id="value-error" role="alert">{{ $message }}</p>
@enderror
<brok:button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</brok:button>
</form>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([])
{{--
Block Handle — the gutter control cluster beside a content block in an
<x-ui.editor>. Two controls:
1. "+" — inserts a fresh paragraph after the block.
2. Grip — click opens the block menu (turn into, move, duplicate, delete);
press and drag reorders the block, with <x-ui.editor.drop-indicator> marking
the gap it will land in.
Inside an editor it stamps `data-gutter` on the editor root (which opens a
start gutter for it) and follows the hovered block: the editor's `handle`
state carries the block's offset box. Standalone (no editor ancestor) it
renders inline and its events bubble to nothing — harmless.
All interactive controls use @mousedown.prevent so clicking them never blurs
the contenteditable region and drops the current selection.
--}}
<div
data-slot="block-handle"
x-data="{
inEditor: false,
dragged: false,
init() {
const editor = this.$el.closest('[data-slot="editor"]');
if (editor) {
editor.setAttribute('data-gutter', '');
this.inEditor = true;
}
},
onGripDown(event) {
if (event.button !== 0) return;
this.dragged = false;
const onMove = () => { this.dragged = true; };
window.addEventListener('pointermove', onMove, { once: true });
window.addEventListener('pointerup', () => window.removeEventListener('pointermove', onMove), { once: true });
this.$dispatch('editor-block-drag', { event });
},
}"
x-bind:class="inEditor ? 'absolute start-2 z-10 transition-[top,opacity] duration-150 ease-out motion-reduce:transition-none' : 'relative'"
x-bind:style="inEditor && typeof handle !== 'undefined' ? `top:${handle.top + Math.max(0, (Math.min(handle.height, 28) - 24) / 2)}px; opacity:${handle.visible ? 1 : 0}; pointer-events:${handle.visible ? 'auto' : 'none'}` : ''"
{{ $attributes->merge(['class' => 'inline-flex items-center gap-0.5']) }}
>
{{-- ── 1. Insert block button ─────────────────────────────────────────── --}}
<button
type="button"
aria-label="{{ __('Insert block below') }}"
@mousedown.prevent=""
@click="$dispatch('editor-block-action', { action: 'insert-after' })"
class="inline-flex size-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
>
{{-- Plus icon --}}
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
{{-- ── 2. Grip: drag to reorder, click for the block menu ─────────────── --}}
{{-- The menu is <x-ui.editor.turn-into-menu>, which owns the block-type list; the
grip is its trigger and the block-level actions follow the list. --}}
<x-ui.editor.turn-into-menu content-class="w-52">
<x-slot:trigger>
<x-ui.dropdown.trigger
aria-label="{{ __('Block options (drag to move)') }}"
@mousedown.prevent=""
@pointerdown="onGripDown($event)"
@click.capture="if (dragged) { $event.stopImmediatePropagation(); dragged = false }"
class="inline-flex size-6 cursor-grab touch-none items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring active:cursor-grabbing"
>
{{-- Grip-vertical icon --}}
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="9" cy="5" r="1" fill="currentColor" stroke="none"/>
<circle cx="9" cy="12" r="1" fill="currentColor" stroke="none"/>
<circle cx="9" cy="19" r="1" fill="currentColor" stroke="none"/>
<circle cx="15" cy="5" r="1" fill="currentColor" stroke="none"/>
<circle cx="15" cy="12" r="1" fill="currentColor" stroke="none"/>
<circle cx="15" cy="19" r="1" fill="currentColor" stroke="none"/>
</svg>
</x-ui.dropdown.trigger>
</x-slot:trigger>
<x-ui.dropdown.separator />
{{-- ── Block-level actions ─────────────────────────────────────── --}}
<x-ui.dropdown.item type="button" @mousedown.prevent="" @click="$dispatch('editor-block-action', { action: 'move-up' })">
<svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7M12 19V5"/></svg>
{{ __('Move up') }}
</x-ui.dropdown.item>
<x-ui.dropdown.item type="button" @mousedown.prevent="" @click="$dispatch('editor-block-action', { action: 'move-down' })">
<svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14m7-7-7 7-7-7"/></svg>
{{ __('Move down') }}
</x-ui.dropdown.item>
<x-ui.dropdown.item type="button" @mousedown.prevent="" @click="$dispatch('editor-block-action', { action: 'duplicate' })">
<svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{{ __('Duplicate') }}
</x-ui.dropdown.item>
<x-ui.dropdown.separator />
<x-ui.dropdown.item
type="button"
@mousedown.prevent=""
@click="$dispatch('editor-block-action', { action: 'delete' })"
class="text-destructive focus:bg-destructive/10 focus:text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/>
<path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
{{ __('Delete') }}
</x-ui.dropdown.item>
</x-ui.editor.turn-into-menu>
</div>
{{-- <x-ui.block-handle> is the install slug's compatibility tag: the part lives at
<x-ui.editor.block-handle> (ui/editor/block-handle) with every other editor part.
Compose the namespaced tag in new code; this file only forwards to it. --}}
@props([])
<x-ui.editor.block-handle {{ $attributes }} />
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.4.2 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0