Skip to content
Brok UI

Loading…

No results

Editor Toolbar

Open source

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.

Version
v1.3.3
Stability
stable
License
MIT
Related
Editor
Color Dropdown
Emoji Picker

Preview

Adding page…

previews.components.editor-toolbar.default.blade.php 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>

Installation

terminal
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:

resources/js/ui/index.js JS
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.

  • blade resources/views/components/ui/editor/toolbar.blade.php
  • blade resources/views/components/ui/editor-toolbar.blade.php
Registry dependencies
button separator dropdown turn-into-menu editor
Packages
composer: jml/brok:^0.2
npm: alpinejs

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.

editor-toolbar.md
# 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

floating.blade.php Blade
<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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

{{ $slotName }}

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

Rich authoring and editing

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
root
Theming hooks
{{ $slotName }}

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<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.

resources/views/components/ui/editor/toolbar.blade.php Blade
@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
resources/views/components/ui/editor-toolbar.blade.php Blade
{{-- <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