Skip to content
Brok UI

Loading…

No results

Link Toolbar

Open source

Floating link editor toolbar with two modes: an editing state (URL input + Apply) and a preview state (URL chip, Open in new tab, Edit, Unlink). Integrates with the editor contract via editor-exec events. Composed as <x-ui.editor.link-toolbar>; <x-ui.link-toolbar> stays as the compatibility tag.

Version
v1.3.3
Stability
stable
License
MIT
Related
Editor Toolbar
Editor Media

Preview

previews.components.link-toolbar.default.blade.php Blade
<div class="flex w-full flex-col items-center gap-6 p-6">
    <x-ui.editor.link-toolbar />
    <x-ui.editor.link-toolbar :editing="false" url="https://brokui.dev/docs/components/link-toolbar" />
</div>

Installation

terminal
php artisan ui:add link-toolbar

Note

This component ships an Alpine behavior module at resources/js/ui/link-toolbar.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './link-toolbar.js';

Registry contract

php artisan ui:add link-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/link-toolbar.blade.php
  • blade resources/views/components/ui/link-toolbar.blade.php
Registry dependencies
button separator 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.

link-toolbar.md
# Brok UI: Link Toolbar (`link-toolbar`)

Floating link editor toolbar with two modes: an editing state (URL input + Apply) and a preview state (URL chip, Open in new tab, Edit, Unlink). Integrates with the editor contract via editor-exec events. Composed as <x-ui.editor.link-toolbar>; <x-ui.link-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 link-toolbar
```

## Usage

```blade
<div class="flex w-full flex-col items-center gap-6 p-6">
    <x-ui.editor.link-toolbar />
    <x-ui.editor.link-toolbar :editing="false" url="https://brokui.dev/docs/components/link-toolbar" />
</div>
```

## Props

- `editing` (bool, default `true`) — Starts in the editing state (URL field) when true, or the preview chip state when false.
- `url` (string, default ``) — Current link URL shown in the field or chip.

## Use when

- Use when users need rich structured authoring, annotation, or collaborative editing controls.
- Editing or previewing a link inside <x-ui.editor>, either while composing a new link or reviewing one already applied.

## Avoid when

- Do not use rich editor affordances for simple one-line or plain-text tasks.
- A permanent, always-visible link field is wanted outside an editor selection context; use input directly instead.

## Anti-patterns

- Rich editors for one-line or easily validated input

## Rules

- Use the `<brok:link-toolbar>` tag (or `<x-ui.link-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/link-toolbar
- Registry JSON (files, props, contract): https://brokui.dev/r/open/link-toolbar.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
editing bool true Starts in the editing state (URL field) when true, or the preview chip state when false.
url string Current link URL shown in the field or chip.

Slots

  • default —
  • x-ui.editor.link-toolbar — Installed subcomponent from the registry item.

Data slots

Stable hooks for CSS overrides and browser tests.

link-toolbar

Behavior

  • editing toggles between a URL input with an Apply button and a preview chip with Open, Edit, and Unlink actions; Enter applies the URL and Escape returns to preview.
  • Applying dispatches editor-exec with command link and the normalized URL, prepending https:// to schemeless input; Unlink dispatches editor-exec with command unlink. The preview chip links a url prop only when it is relative or uses http, https, mailto or tel.
  • 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.
  • Editing or previewing a link inside <x-ui.editor>, either while composing a new link or reviewing one already applied.

Avoid when

  • Do not use rich editor affordances for simple one-line or plain-text tasks.
  • A permanent, always-visible link field is wanted outside an editor selection context; use input directly instead.

Use instead

  • Textarea or plain field for simple content

Anti-patterns

  • Rich editors for one-line or easily validated input
Anatomy
root
Theming hooks
link-toolbar

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
Enter Escape
Focus
managed
  • Every interactive control uses @mousedown.prevent to preserve the editor's text selection before the click fires.
  • 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="link-toolbar-{{ $record->id }}">
    <div class="flex w-full flex-col items-center gap-6 p-6">
        <x-ui.editor.link-toolbar />
        <x-ui.editor.link-toolbar :editing="false" url="https://brokui.dev/docs/components/link-toolbar" />
    </div>
</div>

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:link-toolbar
        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.

resources/views/components/ui/editor/link-toolbar.blade.php Blade
@props([
    // Start in the editing state (a URL field) or the preview state (the chip).
    'editing' => true,
    'url' => '',
])

{{--
    Link Toolbar — floating link editor for use with <x-ui.editor>.

    Two states toggled by the local `editing` flag:

      editing=true  → a URL field with a leading link glyph and an Apply button
                      (Enter applies, Escape returns to the preview).
      editing=false → the link as a chip (domain in the foreground, path muted)
                      plus Open / Edit / Unlink actions.

    All interactive controls use @mousedown.prevent to preserve the editor
    selection before the click fires. Dispatches:
      editor-exec { command: 'link',   value: url }  — apply / update a link
      editor-exec { command: 'unlink' }              — remove the link

    Standalone (no editor ancestor) the events no-op harmlessly.
--}}
<div
    data-slot="link-toolbar"
    x-data="{
        url: @js($url),
        editing: @js((bool) $editing),
        apply() {
            const value = this.url.trim();
            if (!value) return;
            this.url = /^(https?:|mailto:|tel:|\/|#)/i.test(value) ? value : 'https://' + value;
            this.$dispatch('editor-exec', { command: 'link', value: this.url });
            this.editing = false;
        },
        // A url handed in by the host may be a script URL: link it only when
        // its scheme is http(s), mailto or tel, or it is relative.
        get href() {
            const scheme = this.url.replace(/[\u0000-\u0020]/g, '').match(/^([a-z][a-z0-9+.-]*):/i);
            return !scheme || ['http', 'https', 'mailto', 'tel'].includes(scheme[1].toLowerCase()) ? this.url : null;
        },
        get host() { try { return new URL(this.url, location.href).host.replace(/^www\./, ''); } catch (e) { return this.url; } },
        get path() { try { const u = new URL(this.url, location.href); return (u.pathname + u.search + u.hash).replace(/^\/$/, ''); } catch (e) { return ''; } },
    }"
    x-bind:data-state="editing ? 'editing' : 'preview'"
    {{ $attributes->merge(['class' => 'inline-flex h-11 max-w-full items-center gap-1 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md']) }}
>

    {{-- ── Editing state: one field that reads as a single control ─────────── --}}
    <div x-show="editing" class="flex min-w-0 items-center gap-1">
        <label class="flex h-9 min-w-0 items-center gap-2 rounded-sm px-2 text-muted-foreground focus-within:bg-accent/60">
            <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="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"/>
            </svg>
            <span class="sr-only">{{ __('URL') }}</span>
            <input
                type="url"
                x-model="url"
                x-ref="field"
                placeholder="{{ __('Paste or type a link…') }}"
                autocomplete="off"
                spellcheck="false"
                class="h-full w-64 min-w-0 max-w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
                @keydown.enter.prevent="apply()"
                @keydown.escape.prevent="if (url) editing = false"
            />
        </label>

        <x-ui.button
            variant="default"
            size="sm"
            type="button"
            @mousedown.prevent=""
            @click="apply()"
            ::disabled="!url.trim()"
            class="h-9 shrink-0 px-3"
        >{{ __('Apply') }}</x-ui.button>
    </div>

    {{-- ── Preview state: the link as a chip plus its actions ─────────────── --}}
    <div x-show="!editing" x-cloak class="flex min-w-0 items-center gap-1">
        <a
            x-bind:href="href"
            target="_blank"
            rel="noopener noreferrer"
            class="flex h-9 min-w-0 max-w-72 items-center gap-2 rounded-sm px-2 text-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring"
            @mousedown.prevent=""
        >
            <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">
                <circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
            </svg>
            <span class="truncate">
                <span class="font-medium text-foreground" x-text="host"></span><span class="text-muted-foreground" x-text="path"></span>
            </span>
        </a>

        <x-ui.separator orientation="vertical" :decorative="true" class="mx-0.5 h-5" />

        {{-- Open in new tab --}}
        <a
            x-bind:href="href"
            target="_blank"
            rel="noopener noreferrer"
            aria-label="{{ __('Open link in new tab') }}"
            @mousedown.prevent=""
            class="inline-flex size-9 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"
        >
            <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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
                <polyline points="15 3 21 3 21 9"/>
                <line x1="10" y1="14" x2="21" y2="3"/>
            </svg>
        </a>

        {{-- Edit --}}
        <x-ui.button
            variant="ghost"
            size="icon"
            type="button"
            aria-label="{{ __('Edit link') }}"
            @mousedown.prevent=""
            @click="editing = true; $nextTick(() => $refs.field?.focus())"
            class="size-9 rounded-sm"
        >
            <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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
            </svg>
        </x-ui.button>

        {{-- Unlink --}}
        <x-ui.button
            variant="ghost"
            size="icon"
            type="button"
            aria-label="{{ __('Remove link') }}"
            @mousedown.prevent=""
            @click="$dispatch('editor-exec', { command: 'unlink' }); url = ''; editing = true"
            class="size-9 rounded-sm text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:ring-destructive"
        >
            <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="M18.84 12.25l1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"/>
                <path d="M5.17 11.75l-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"/>
                <line x1="8" y1="2" x2="8" y2="5"/>
                <line x1="2" y1="8" x2="5" y2="8"/>
                <line x1="16" y1="19" x2="16" y2="22"/>
                <line x1="19" y1="16" x2="22" y2="16"/>
            </svg>
        </x-ui.button>
    </div>
</div>
resources/views/components/ui/link-toolbar.blade.php Blade
{{-- <x-ui.link-toolbar> is the install slug's compatibility tag: the part lives at
     <x-ui.editor.link-toolbar> (ui/editor/link-toolbar) with every other editor part.
     Compose the namespaced tag in new code; this file only forwards to it. --}}
@props([
    // Start in the editing state (a URL field) or the preview state (the chip).
    'editing' => true,
    'url' => '',
])

<x-ui.editor.link-toolbar :editing="$editing" :url="$url" {{ $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