Drop a tag on each component and editors always know exactly what they're editing — no matter how deeply nested.
- Two-way sync — click or hover in the Live Preview or Control Panel and the other side highlights instantly
- Auto-expand — click in the Live Preview and the matching set opens and scrolls into view in the Control Panel
- Zero production footprint — annotations and scripts are stripped outside of Live Preview
[!IMPORTANT] This is the installable package repository — it is auto-synced from the development repository, which contains a full demo including tests. Please open issues and PRs there, not here.
Demo
https://github.com/user-attachments/assets/97ec557d-2642-4e74-87df-fb365a03154b
Requirements
- Statamic 6
- PHP 8.4+
Installation
composer require mariohamann/statamic-visual-editor
Publish assets (required on every addon update):
php artisan vendor:publish --provider="MarioHamann\StatamicVisualEditor\ServiceProvider" --force
A settings page is available at CP → Tools → Visual Editor to enable or disable the addon.
Laravel Boost Support
This addon includes three dedicated AI agent skills to help you annotate templates with Visual Editor tags:
visual-editor-research— Audits your project to find where annotations should be added, scanning blueprints, fieldsets, and templates to map sets to partials.visual-editor-antlers— Provides implementation guidance for adding tags to Antlers templates, with examples and parameter reference.visual-editor-blade— Provides implementation guidance for adding tags to Blade templates, including component patterns and blueprint resolution.
When you install/update the addon in a Laravel Boost-enabled project (php artisan boost:update), these skills are automatically made available to your IDE's AI agent. The addon also extends the project's AGENTS.md with core concepts and activation triggers.
For details, see the Boost documentation.
Concepts
The addon provides a single tag — {{ visual_edit }} — that you place on HTML elements in your templates. During Live Preview it outputs data attributes that power bidirectional click-and-hover sync between the preview and the CP. Outside Live Preview it outputs nothing.
There are two targeting modes:
| Mode | What it targets | How it works |
|---|---|---|
| Set targeting | Replicator, Bard & Grid items | Links each rendered item to its CP set via an auto-generated UUID |
| Field targeting | Fixed blueprint fields (title, SEO, etc.) | Links any element to a CP field by its handle |
Both modes are fully bidirectional: clicking or hovering in the preview highlights the CP field, and vice versa.
Set targeting
Targets individual Replicator, Bard, or Grid items. The addon automatically adds a hidden _visual_id field to every set in your blueprints and stamps a stable UUID during preview and on save — no blueprint changes required.
Antlers
Add {{ visual_edit }} to the outermost element of each set partial. The tag reads _visual_id and type from the current context automatically:
{{# Replicator / Bard set partial #}}<div class="..." {{ visual_edit }}> {{ text }}</div>
{{# Grid rows #}}{{ links }} <li {{ visual_edit }}> <a href="{{ link_url }}">{{ label }}</a> </li>{{ /links }}
Blade
Use Statamic::tag('visual_edit') with ->context($item->all()) to pass the set/row data. The tag reads _visual_id and type from the context, just like in Antlers:
{{-- Replicator / Bard set --}}<div {!! Statamic::tag('visual_edit')->context($set->all())->fetch() !!}> {!! $set->text !!}</div>
{{-- Grid rows --}}@foreach ($rows as $row) <li {!! Statamic::tag('visual_edit')->context($row->all())->fetch() !!}> {!! (string) ($row->rule ?? '') !!} </li>@endforeach
Important: Always use
{!! !!}(unescaped output), not{{ }}. The tag returns raw HTML attributes.
Field targeting
Targets fixed blueprint fields — titles, SEO metadata, or any field that isn't inside a Replicator/Bard/Grid. The CP jumps directly to the field when clicked, switching tabs automatically if needed.
Antlers
{{# Top-level field #}}<h1 {{ visual_edit field="hero_title" }}>{{ hero_title }}</h1> {{# Nested field inside a group (dot notation) #}}<p {{ visual_edit field="page_info.author" }}>{{ page_info:author }}</p>
The tooltip label is resolved from the field's Display Name in the current entry's blueprint automatically.
Blade
{{-- Recommended: pass the blueprint handle (works without an entry object) --}}<h1 {!! Statamic::tag('visual_edit')->blueprint('collections.pages')->field('hero_title')->fetch() !!}> {{-- Alternative: pass the entry for blueprint resolution --}}<h1 {!! Statamic::tag('visual_edit')->context(['page' => $entry])->field('hero_title')->fetch() !!}> {{-- Minimal: no label resolution (CP navigation still works; label is cosmetic) --}}<h1 {!! Statamic::tag('visual_edit')->field('hero_title')->fetch() !!}>
The blueprint parameter accepts a namespaced handle: collections.{handle}, globals.{handle}.
Tip: In Blade components you often don't have the entry object — use
->blueprint()instead of threading$entrythrough props.
Dot notation
Use dots to target nested fields inside groups: page_info.author. Avoid top-level field handles containing underscores that could collide with group subfield paths — both page_info.author and page_info_author resolve to the same CP element ID.
Additional features
Pair tag
When there's no single outermost element to annotate, use the pair tag to wrap content in a <div>:
{{ visual_edit }} <h1>{{ hero_title }}</h1> <p>{{ hero_text }}</p>{{ /visual_edit }}
Outline inside
For dense layouts where a 2 px outbound outline overlaps neighbouring elements, draw the outline inside instead:
<div {{ visual_edit outline-inside="true" }}>
<div {!! Statamic::tag('visual_edit')->context($set->all())->params(['outline-inside' => true])->fetch() !!}>
Parameter reference
All parameters work in both Antlers and Blade (via the fluent API).
| Parameter | Default | Description |
|---|---|---|
| (none) | — | Auto-targets the current set by its UUID |
field |
— | Targets a fixed field by handle (dot notation for nested groups) |
blueprint |
— | Resolve field labels from a specific blueprint (e.g. collections.pages). In Antlers the entry's blueprint is used automatically. |
outline-inside |
false |
Draws the outline inside the element border |
id |
— | Override: target a specific set by a known UUID |
Antlers ↔ Blade mapping
| Antlers | Blade |
|---|---|
{{ visual_edit }} |
Statamic::tag('visual_edit')->context($set->all())->fetch() |
{{ visual_edit field="title" }} |
Statamic::tag('visual_edit')->field('title')->fetch() |
{{ visual_edit field="title" blueprint="collections.pages" }} |
Statamic::tag('visual_edit')->blueprint('collections.pages')->field('title')->fetch() |
{{ visual_edit outline-inside="true" }} |
Statamic::tag('visual_edit')->context($set->all())->params(['outline-inside' => true])->fetch() |
Developer reference
How it works
- Blueprint injection —
InjectVisualIdIntoBlueprintadds a hidden_visual_idfield (typeauto_uuid) to every Replicator, Bard, and Grid set when a blueprint is loaded. - Ephemeral UUID generation — When the CP form loads,
AutoUuidFieldtype::preProcess()generates a fresh UUID in-memory for any set that doesn't already have one. UUIDs are never persisted —StripVisualIdsremoves any_visual_idvalues from the data before saving. - Template annotation —
{{ visual_edit }}outputsdata-sid="{uuid}"(set targeting) ordata-sid-field="{path}"(field targeting) plus optional label/type attributes. - Bridge script —
InjectBridgeScriptmiddleware injectsbridge.jsinto the Live Preview iframe. It handles click/hover events and communicates with the CP viapostMessage. - CP script —
addon.js(loaded via Vite) listens for messages from the iframe, expands collapsed sets, switches tabs, scrolls, and highlights the target field.
Because the CP form and the Live Preview share the same in-memory form state, the ephemeral UUIDs are identical on both sides for the duration of the editing session — no persistence is needed. Hover sync works in both directions for both mechanisms.