Editor Toolbar
Formatting toolbar for the rich-text editor. `variant="fixed"` (default) carries the block-type dropdown (turn-into-menu), mark buttons (bold/italic/underline/strikethrough/code), list/quote/link controls and a More overflow menu; `variant="floating"` is the compact selection bubble with the marks and Link. Composed as <x-ui.editor.toolbar>; <x-ui.editor-toolbar> stays as the compatibility tag.
Preview
Formatting toolbar
Select text and use the toolbar to apply marks, change the block type, or insert a link.
Insert image
Describe the image for people who cannot see it.
Uploading image…
Adding page…
<x-ui.editor class="mx-auto max-w-2xl" placeholder="Write something…">
<x-slot:toolbar>
<x-ui.editor.toolbar />
</x-slot:toolbar>
<h2>Formatting toolbar</h2>
<p>Select text and use the toolbar to apply <strong>marks</strong>, change the block type, or insert a link.</p>
</x-ui.editor>
Installation
php artisan ui:add editor-toolbar
Note
This component ships an Alpine behavior module at
resources/js/ui/editor-toolbar.js. Import it once from your bundle so it registers on alpine:init:
import './editor-toolbar.js';
Registry contract
php artisan ui:add editor-toolbar
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/toolbar.blade.php -
resources/views/components/ui/editor-toolbar.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: Editor Toolbar (`editor-toolbar`)
Formatting toolbar for the rich-text editor. `variant="fixed"` (default) carries the block-type dropdown (turn-into-menu), mark buttons (bold/italic/underline/strikethrough/code), list/quote/link controls and a More overflow menu; `variant="floating"` is the compact selection bubble with the marks and Link. Composed as <x-ui.editor.toolbar>; <x-ui.editor-toolbar> 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 editor-toolbar
```
## Usage
```blade
<x-ui.editor class="mx-auto max-w-2xl" placeholder="Write something…">
<x-slot:toolbar>
<x-ui.editor.toolbar />
</x-slot:toolbar>
<h2>Formatting toolbar</h2>
<p>Select text and use the toolbar to apply <strong>marks</strong>, change the block type, or insert a link.</p>
</x-ui.editor>
```
## Props
- `variant` (string, default `fixed`) — Layout: fixed (docked toolbar, default) or floating (selection bubble).
## Use when
- Use when users need rich structured authoring, annotation, or collaborative editing controls.
- Adding the editor's formatting controls: a block-type dropdown, mark buttons, list/link/quote controls, and a More overflow menu.
- Choosing the fixed toolbar docked above the document, or the floating selection bubble that appears over a text selection.
## Avoid when
- Do not use rich editor affordances for simple one-line or plain-text tasks.
- A single, ad-hoc formatting control is needed outside an editor context; compose plain buttons instead of the whole toolbar.
## Anti-patterns
- Rich editors for one-line or easily validated input
## Rules
- Use the `<brok:editor-toolbar>` tag (or `<x-ui.editor-toolbar>`) 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/editor-toolbar
- Registry JSON (files, props, contract): https://brokui.dev/r/open/editor-toolbar.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
<x-ui.editor class="mx-auto max-w-2xl" placeholder="Write something…">
<x-slot:tools>
<x-ui.editor.toolbar variant="floating" />
</x-slot:tools>
<p>Highlight any run of text in this editor and the floating variant appears above the
selection with the common marks. Try selecting <strong>these few words</strong>.</p>
</x-ui.editor>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | string | fixed | Layout: fixed (docked toolbar, default) or floating (selection bubble). |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- variant="fixed" renders in the editor's toolbar slot and stays visible; variant="floating" renders in the tools slot, hidden until the editor's floating flag turns true for a settled, non-collapsed selection.
- Both variants read the shared uiEditor Alpine scope, isActive, isBlock, blockType, and selection, from the ancestor editor, so toolbar state always reflects the live selection.
- 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.
- Adding the editor's formatting controls: a block-type dropdown, mark buttons, list/link/quote controls, and a More overflow menu.
- Choosing the fixed toolbar docked above the document, or the floating selection bubble that appears over a text selection.
Avoid when
- Do not use rich editor affordances for simple one-line or plain-text tasks.
- A single, ad-hoc formatting control is needed outside an editor context; compose plain buttons instead of the whole toolbar.
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
- managed
- Focus
none
- Every control uses @mousedown.prevent so clicking a formatting button never blurs the editor 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="editor-toolbar-{{ $record->id }}">
<x-ui.editor class="mx-auto max-w-2xl" placeholder="Write something…">
<x-slot:toolbar>
<x-ui.editor.toolbar />
</x-slot:toolbar>
<h2>Formatting toolbar</h2>
<p>Select text and use the toolbar to apply <strong>marks</strong>, change the block type, or insert a link.</p>
</x-ui.editor>
</div>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'variant' => 'fixed',
])
{{--
Editor Toolbar — the rich-text editor's formatting controls.
`variant="fixed"` (default) sits in <x-ui.editor>'s $toolbar slot: the
block-type dropdown (composed from <x-ui.editor.turn-into-menu>, which owns the
block-type list), the mark buttons, list / link / quote controls and a
"More" overflow menu.
`variant="floating"` is the compact selection bubble for the $tools slot:
the mark buttons plus Link. It is hidden by default (x-show + x-cloak) and
shows on the editor's `floating` flag — true only once a non-collapsed
selection has settled (pointer up, keys released) — positioned above the
selected text (below it when there is no room above) with fixed
positioning from the selection rect.
Both read the shared uiEditor Alpine scope (`isActive`, `isBlock`,
`blockType`, `selection`, `floating`) through the parent x-data="uiEditor()".
@mousedown.prevent on every control preserves the editor selection before
the click fires.
--}}
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
$variant = $variant === 'floating' ? 'floating' : 'fixed';
// An alias (editor-floating-toolbar) passes its own data-slot; the browser
// keeps only the first of two identical attributes, so print exactly one.
$slotName = (string) ($attributes->get('data-slot') ?: 'editor-toolbar');
$attributes = $attributes->except('data-slot');
$activeClass = 'data-[active=true]:bg-accent data-[active=true]:text-accent-foreground';
// One icon set for the mark buttons, shared by both variants. Each entry is
// [command, label, stroke-width, inner SVG].
$marks = [
['bold', __('Bold'), '2.5', '<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>'],
['italic', __('Italic'), '2', '<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>'],
['underline', __('Underline'), '2', '<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/>'],
['strikethrough', __('Strikethrough'), '2', '<line x1="4" y1="12" x2="20" y2="12"/><path d="M17.5 6.5C17.5 4.6 15.7 3 13.5 3H10C7.8 3 6 4.6 6 6.5c0 2.3 2.2 3.2 5 3.5"/><path d="M6.5 17.5C6.5 19.4 8.3 21 10.5 21H14c2.2 0 4-1.6 4-3.5"/>'],
['code', __('Inline code'), '2', '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'],
];
$linkIcon = '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>';
@endphp
@if ($variant === 'floating')
<div
data-slot="{{ $slotName }}"
data-variant="floating"
role="toolbar"
aria-label="{{ __('Quick formatting') }}"
x-show="typeof floating !== 'undefined' && floating"
x-cloak
x-transition:enter="transition duration-150 ease-out motion-reduce:transition-none"
x-transition:enter-start="scale-95 opacity-0"
x-transition:enter-end="scale-100 opacity-100"
x-transition:leave="transition duration-100 ease-in motion-reduce:transition-none"
x-transition:leave-start="scale-100 opacity-100"
x-transition:leave-end="scale-95 opacity-0"
{{-- Object form on purpose: a string binding would overwrite the
`display: none` x-show writes into the same attribute. --}}
x-bind:style="(typeof selection !== 'undefined' && selection && selection.rect)
? { position: 'fixed', zIndex: 50, top: (selection.rect.top > 56 ? selection.rect.top - 48 : selection.rect.bottom + 8) + 'px', left: Math.max(8, Math.min(window.innerWidth - 8, selection.rect.left + selection.rect.width / 2)) + 'px', transform: 'translateX(-50%)' }
: {}"
{{ $attributes->merge(['class' => 'flex origin-bottom items-center gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md']) }}
>
@else
<div
data-slot="{{ $slotName }}"
data-variant="fixed"
role="toolbar"
aria-label="{{ __('Text formatting') }}"
{{ $attributes->merge(['class' => 'flex flex-wrap items-center gap-1 p-2 bg-background']) }}
>
{{-- ── 1. Block-type "Turn into" dropdown ───────────────────────────── --}}
<x-ui.editor.turn-into-menu trigger="current" />
<x-ui.separator orientation="vertical" :decorative="true" class="mx-1 h-6" />
@endif
{{-- ── 2. Mark buttons: Bold, Italic, Underline, Strikethrough, Inline code ── --}}
@foreach ($marks as [$command, $markLabel, $strokeWidth, $glyph])
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ $markLabel }}"
@mousedown.prevent=""
@click="$dispatch('editor-exec', { command: {{ Js::from($command) }} })"
x-bind:aria-pressed="typeof isActive === 'function' && isActive({{ Js::from($command) }})"
x-bind:data-active="typeof isActive === 'function' && isActive({{ Js::from($command) }})"
:class="$activeClass"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $glyph !!}</svg>
</x-ui.button>
@endforeach
@if ($variant === 'floating')
{{-- Link --}}
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ __('Link') }}"
@mousedown.prevent=""
@click="$dispatch('editor-exec', { command: 'link' })"
x-bind:aria-pressed="typeof isActive === 'function' && isActive('link')"
x-bind:data-active="typeof isActive === 'function' && isActive('link')"
:class="$activeClass"
>
<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">{!! $linkIcon !!}</svg>
</x-ui.button>
</div>
@else
<x-ui.separator orientation="vertical" :decorative="true" class="mx-1 h-6" />
{{-- ── 3. List buttons ───────────────────────────────────────────────── --}}
{{-- Bulleted list --}}
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ __('Bulleted list') }}"
@mousedown.prevent=""
@click="$dispatch('editor-block', { type: 'bulleted' })"
x-bind:aria-pressed="typeof isBlock === 'function' && isBlock('bulleted')"
x-bind:data-active="typeof isBlock === 'function' && isBlock('bulleted')"
:class="$activeClass"
>
<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="9" y1="6" x2="20" y2="6"/>
<line x1="9" y1="12" x2="20" y2="12"/>
<line x1="9" y1="18" x2="20" y2="18"/>
<circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/>
<circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/>
<circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>
</svg>
</x-ui.button>
{{-- Numbered list --}}
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ __('Numbered list') }}"
@mousedown.prevent=""
@click="$dispatch('editor-block', { type: 'numbered' })"
x-bind:aria-pressed="typeof isBlock === 'function' && isBlock('numbered')"
x-bind:data-active="typeof isBlock === 'function' && isBlock('numbered')"
:class="$activeClass"
>
<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="10" y1="6" x2="21" y2="6"/>
<line x1="10" y1="12" x2="21" y2="12"/>
<line x1="10" y1="18" x2="21" y2="18"/>
<path d="M4 6h1v4" stroke-linecap="round"/>
<path d="M4 10h2" stroke-linecap="round"/>
<path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" stroke-linecap="round"/>
</svg>
</x-ui.button>
<x-ui.separator orientation="vertical" :decorative="true" class="mx-1 h-6" />
{{-- ── 4. Link & Quote ──────────────────────────────────────────────── --}}
{{-- Link --}}
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ __('Link') }}"
@mousedown.prevent=""
@click="$dispatch('editor-exec', { command: 'link' })"
x-bind:aria-pressed="typeof isActive === 'function' && isActive('link')"
x-bind:data-active="typeof isActive === 'function' && isActive('link')"
:class="$activeClass"
>
<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">{!! $linkIcon !!}</svg>
</x-ui.button>
{{-- Blockquote --}}
<x-ui.button
variant="ghost"
size="icon"
type="button"
aria-label="{{ __('Quote') }}"
@mousedown.prevent=""
@click="$dispatch('editor-block', { type: 'blockquote' })"
x-bind:aria-pressed="typeof isBlock === 'function' && isBlock('blockquote')"
x-bind:data-active="typeof isBlock === 'function' && isBlock('blockquote')"
:class="$activeClass"
>
<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">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>
</svg>
</x-ui.button>
<x-ui.separator orientation="vertical" :decorative="true" class="mx-1 h-6" />
{{-- ── 5. "More" overflow dropdown ──────────────────────────────────── --}}
<x-ui.dropdown>
<x-ui.dropdown.trigger
class="inline-flex size-8 items-center justify-center rounded-md 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"
aria-label="{{ __('More options') }}"
@mousedown.prevent=""
>
<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="12" cy="5" r="1" fill="currentColor" stroke="none"/>
<circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/>
<circle cx="12" cy="19" r="1" fill="currentColor" stroke="none"/>
</svg>
</x-ui.dropdown.trigger>
<x-ui.dropdown.content align="end">
<x-ui.dropdown.item
type="button"
@mousedown.prevent=""
@click="$dispatch('editor-exec', { command: 'removeFormat' })"
>
<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">
<path d="M4 7V4h16v3"/>
<path d="M5 20h6"/>
<path d="M13 4l-4 16"/>
<line x1="3" y1="3" x2="21" y2="21"/>
</svg>
{{ __('Clear formatting') }}
</x-ui.dropdown.item>
<x-ui.dropdown.item
type="button"
@mousedown.prevent=""
@click="$dispatch('editor-insert', { html: '<hr>' })"
>
<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">
<line x1="3" y1="12" x2="21" y2="12"/>
</svg>
{{ __('Insert divider') }}
</x-ui.dropdown.item>
</x-ui.dropdown.content>
</x-ui.dropdown>
</div>
@endif
{{-- <x-ui.editor-toolbar> is the install slug's compatibility tag: the part lives at
<x-ui.editor.toolbar> (ui/editor/toolbar) with every other editor part.
Compose the namespaced tag in new code; this file only forwards to it. --}}
@props([
'variant' => 'fixed',
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
@endphp
<x-ui.editor.toolbar :variant="$variant" {{ $attributes }} />
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.3 - 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