Skip to main content
WireKit
Copy for LLM

Editor

<x-wirekit::editor> is a rich-text editing surface that drops into a Livewire form the same way <x-wirekit::textarea> does — a Blade tag, a hidden form field, wire:model support, theme-token styling, full keyboard access. It is a thin adapter around Tiptap (built on ProseMirror); Tiptap itself stays a peer dependency you install from npm, exactly like the chart adapter. WireKit ships only the lightweight Alpine glue.

Installation

Tiptap is not bundled. Install the core plus whatever extensions you need, then expose a window.wirekitEditor(config) factory that returns a Tiptap Editor. The factory name is engine-neutral — any ProseMirror-based Editor works — and the legacy window.tiptapEditor name is still accepted as a deprecated alias (renaming is a one-line change):

# 1. Install the Tiptap core + starter kit (the 80% case).
npm install @tiptap/core @tiptap/starter-kit

# 2. Optional extensions, per form.
npm install @tiptap/extension-link @tiptap/extension-placeholder \
            @tiptap/extension-text-align @tiptap/extension-character-count
// 3. app.js — expose the factory WireKit calls at Alpine init().
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';

window.wirekitEditor = (config) => new Editor({
    // 4. WireKit hands you element / content / editable / callbacks / editorProps.
    element: config.element,
    content: config.content,
    editable: config.editable,
    editorProps: config.editorProps,
    onCreate: config.onCreate,
    onUpdate: config.onUpdate,
    onSelectionUpdate: config.onSelectionUpdate,
    onTransaction: config.onTransaction,
    // 5. YOU own the extension set. `config.extensions` is an OPTIONAL list of
    //    string name-hints (empty by default) — map them to real extensions only
    //    if your factory reads them; never spread the raw strings into Tiptap
    //    (they are not Extension objects). Restrict link protocols to block
    //    javascript: URLs.
    extensions: [
        StarterKit,
        Link.configure({ protocols: ['http', 'https', 'mailto'], openOnClick: false }),
        Placeholder.configure({ placeholder: config.placeholder ?? 'Write something…' }),
    ],
});

Which WireKit bundle registers the editor

The wirekitEditor Alpine factory that <x-wirekit::editor> wires into x-data ships in the full bundle (wirekit.js) and the self-contained bundle (wirekit-alpine.js) — if you load either, the editor works with no extra script. If you load the core bundle (wirekit.core.js, chart-only) to keep your footprint small, add the standalone editor adapter alongside it:

{{-- # 1. Core bundle (no overlays) keeps the base footprint small --}}
@wirekitScripts(bundle: 'core')
{{-- # 2. Editor adapter glue — registers `wirekitEditor` next to core --}}
<script defer src="{{ asset('vendor/wirekit/wirekit-tiptap.js') }}"></script>

The wirekit-tiptap.js adapter is tiny (size in Dependencies) and contains only the editor glue — no Tiptap code (that stays your peer dependency). Loading it next to wirekit.js or wirekit-alpine.js is redundant: those already include the editor. See Choosing a JS bundle for the full bundle matrix.

Your factory must forward config.editorProps (as the example above does). It carries the editable surface's WAI-ARIA role="textbox" + aria-multiline and the wk-editor-content class that applies WireKit's typography, the flex-fill that turns the whole box into the work area, and the inner focus-outline suppression. A factory that drops editorProps mounts an editor with no accessible role and no styling — it still renders, but screen readers announce no textbox and none of the theme rules apply. Forward config.onCreate and config.onTransaction too, so the character counter and toolbar active-state stay in sync.

Factory config contract

WireKit calls window.wirekitEditor(config) with exactly these keys — your factory receives them and wires each into Tiptap:

config key What WireKit passes Your factory should
element The DOM node to mount the editor into pass as Tiptap element
content The initial HTML (the wire:model value) pass as Tiptap content
editable false when the editable prop is false, otherwise true pass as Tiptap editable
extensions An optional array of extension name hints (default []) — never real extension objects own the real extension set; read the hints only if your factory maps them
editorProps ProseMirror editorProps carrying role="textbox", aria-multiline, any aria-label / aria-describedby / aria-invalid, and the wk-editor-content class forward verbatim (required for a11y + styling)
onCreate · onUpdate · onSelectionUpdate · onTransaction Lifecycle callbacks WireKit uses to sync wire:model, the character counter, and toolbar active-state forward each verbatim

WireKit treats this as a stable input contract: existing keys won't change or disappear in a minor or patch release (new keys, if any, arrive as optional additions). Writing your factory straight from this table — rather than guessing from the Alpine source — is what avoids the classic extensions: ['StarterKit'] mistake of spreading name hints into Tiptap as if they were real extension objects.

If no factory is defined (neither window.wirekitEditor nor the legacy window.tiptapEditor alias), the editor gracefully falls back to a plain <textarea> and logs a console error, so the form stays usable either way.

Your factory can return the Editor as-is — WireKit marks it non-reactive (Vue's markRaw flag) before storing it on Alpine state, so Alpine never deep-proxies it. A reactivity-proxied ProseMirror editor would reject every command with a "mismatched transaction" error; the glue rules that whole class out for you.

Sanitize on store. Tiptap's schema strips <script>, inline event handlers, and disallowed nodes when it parses, but that is not a complete XSS defense for the round-trip. Always run a server-side sanitizer (e.g. mews/purifier) on every write path before saving the HTML, and restrict the link extension's protocols to ['http', 'https', 'mailto']. Never render saved HTML on a page without these protections.

Basic Usage

The toolbar="basic" preset (the default) gives you bold / italic / strike / link / lists. The document is written to a hidden input as HTML on every change, so wire:model and native form submission just work.

Basic editor

Edit me — bold, italic, and a link all round-trip through the hidden input.

Toolbar Presets

toolbar="full" exposes headings, quotes, code blocks, and undo/redo; toolbar="false" hides it (keyboard shortcuts still work); toolbar="custom" lets you compose <x-wirekit::editor.toolbar :commands="[...]"> in the toolbar slot.

A few commands live in Tiptap extensions that StarterKit does not bundle: underline (in the full preset) needs @tiptap/extension-underline, and the optional align-* / task-list commands need @tiptap/extension-text-align / @tiptap/extension-task-list. Register each one in your window.wirekitEditor factory before enabling its button. If a command's extension is missing, the editor logs a console hint and keeps working — the click is a no-op rather than a hard error.

Full toolbar

A longer document

The full toolbar adds headings, lists, code blocks, and history.

Read-only

Set :editable="false" to render a document as a non-editable preview (no toolbar, no hidden input) — useful for moderation queues and comment threads.

Read-only document

This document is read-only. There's no toolbar and no form field — just the rendered content.

Character limit

Set maxLength to render a live counter in the bottom bar. It's a soft limit — the count turns red past the limit but typing isn't blocked (for a hard stop, pass a limit to CharacterCount in your factory). The visible count is aria-hidden; a debounced screen-reader announcement speaks the remaining count when the user pauses, so it never narrates every keystroke.

Character counter

The counter sits in the bottom bar and turns red once you pass the limit.

Advanced: mentions, slash commands & collaboration

The extensions array in your window.wirekitEditor factory is the extension point for everything Tiptap can do beyond the toolbar presets. WireKit doesn't bundle these — you add the Tiptap peer extensions you need and wire their config, exactly where you already add StarterKit. Three common ones:

Mentions (@name)

@tiptap/extension-mention turns @ into a typeahead. You own the suggestion source and the popup; WireKit just hosts the editor.

// 1. npm install @tiptap/extension-mention
import Mention from '@tiptap/extension-mention';

// 2. Add it to the factory's extension set (alongside StarterKit, Link, …).
//    The suggestion.items callback is YOUR data source.
Mention.configure({
    HTMLAttributes: { class: 'wk-mention' },
    suggestion: {
        items: ({ query }) =>
            people.filter((p) => p.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
        // 3. render() mounts the suggestion popup — see Tiptap's suggestion utility.
    },
});

Slash commands & AI

A slash menu is the same suggestion mechanism keyed on / instead of @. Build it on @tiptap/suggestion: each item runs an editor command when picked. An "AI" item is just a command that calls your endpoint and inserts the response.

// 1. A slash item that asks YOUR backend to continue the text, then inserts it.
{
    title: 'Continue with AI',
    command: async ({ editor, range }) => {
        // 2. The request, model, auth, and rate-limiting are all yours —
        //    WireKit never calls an AI service.
        const text = await fetch('/ai/continue', { method: 'POST' }).then((r) => r.text());
        editor.chain().focus().deleteRange(range).insertContent(text).run();
    },
}

Collaboration (Yjs)

Real-time collaboration uses @tiptap/extension-collaboration on top of a Yjs document synced by a provider you run.

Collaboration needs a sync backend you provide — a y-websocket server, Hocuspocus, or similar. WireKit ships no server, and a component library cannot: the shared document state lives in your infrastructure. When you wire Collaboration you must let Yjs own the content — do not also pass value or treat wire:model as the source of truth, or the two fight over the document. Use the hidden input only if you still want a snapshot on form submit.

// 1. npm install @tiptap/extension-collaboration yjs y-websocket
import Collaboration from '@tiptap/extension-collaboration';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

// 2. One shared doc + a provider pointed at YOUR server.
const ydoc = new Y.Doc();
new WebsocketProvider('wss://collab.example.com', roomName, ydoc);

// 3. Add Collaboration and DON'T set `content` — Yjs hydrates the document.
//    Collaboration manages undo/redo, so disable StarterKit's own history.
Collaboration.configure({ document: ydoc });

For presence cursors, add @tiptap/extension-collaboration-cursor with each user's name and color.

Props

Prop Type Default Description
name string|null null Form-field name (and the hidden input's name). Required to submit.
value string|array|null null Initial document — an HTML string (format="html") or a JSON document (format="json").
format string 'html' html or json — what's written to the hidden input on every change.
extensions array|null config Editor extension name hints passed to your window.wirekitEditor (empty by default; the factory owns the real extension set).
toolbar bool|string 'basic' false, 'basic', 'full', or 'custom' (use the toolbar slot).
placeholder string|null null Empty-document placeholder (needs @tiptap/extension-placeholder).
maxLength int|null null Renders a live character counter in the bottom bar. Soft limit — it shows an over-limit state but does not block typing; for a hard stop, configure CharacterCount with a limit in your factory. The counter reads @tiptap/extension-character-count when installed, else plain-text length.
editable bool true false renders a read-only preview with no toolbar / hidden input.
label string|null null Renders above via <x-wirekit::label>.
hint string|null null Helper text below.
error string|null null Error message; sets aria-invalid on the content region.
size string 'md' sm / md / lg — minimum content height.
maxHeight string|null null Cap the editable area's height (any CSS length, e.g. '20rem' / '50vh'). The content already scrolls; this gives it a ceiling so a long document scrolls inside the field instead of growing the page downward. Width is set the normal way — the editor is full-width, so constrain it with a class or style on the component (e.g. style="max-width: 40rem").
autofocus bool false Focus the editor on first paint.
scope string|null null Scoped personalization name.

Slots

Slot Description
toolbar Custom toolbar (with toolbar="custom") — usually <x-wirekit::editor.toolbar :commands="[...]">.
bottomBar Optional bottom-edge slot for a character counter or save-status indicator.

Events

The factory dispatches input and change on the hidden field after every (debounced) change, so wire:model.live and legacy listeners both fire.

Accessibility

  • The content region is a role="textbox" with aria-multiline="true"; the label prop names it (or a humanized name fallback).
  • The toolbar is a role="toolbar" labeled "Editor commands"; toggle buttons expose aria-pressed reflecting the current mark/node.
  • Toolbar buttons use mousedown.prevent so clicking a command never steals selection from the editor.
  • The undo and redo buttons disable themselves (disabled + dimmed) when there is nothing to undo or redo, so the toolbar never offers a dead command.
  • Error state sets aria-invalid="true" + aria-describedby to the error text — same as every other WireKit input.
  • Default Tiptap keyboard shortcuts work (see below).

Keyboard Interaction

Key Action
Cmd/Ctrl + B Bold
Cmd/Ctrl + I Italic
Cmd/Ctrl + Shift + X Strikethrough
Cmd/Ctrl + E Inline code
Cmd/Ctrl + Z / Cmd/Ctrl + Shift + Z Undo / redo
Tab / Shift + Tab Indent / outdent inside lists

Design Tokens

Token Used for
--color-wk-bg-input Editor surface
--color-wk-border-strong / --color-wk-border-error Editor border
--color-wk-bg-subtle Toolbar background
--color-wk-ring Focus-within ring
--color-wk-accent Links inside the document
--radius-wk-md Editor corner radius

Usage & Conventions

Tiptap licensing. Tiptap's core (@tiptap/core, @tiptap/starter-kit, and the @tiptap/extension-* packages) is MIT and free. Optional Pro extensions (@tiptap-pro/* — AI, comments, version history) are commercial and namespaced separately. WireKit's adapter targets the MIT core; you can pass any Pro extension through if you've licensed it, while WireKit itself ships only the MIT glue.

Further Reading