Designing the Edit-Mode Experience
Make components feel great on the editing canvas with placeholders, preview data, and editor styles
Your component will spend most of its life on the editing canvas. Users drop it in empty, click into it, rearrange it, and stare at it while making decisions — long before a visitor ever sees the published page. A component that renders perfectly when published but sits blank, broken, or booby-trapped in the editor feels unfinished, no matter how good the shipped output is. The techniques for a great edit-mode experience are scattered across the reference docs; this guide pulls them into one place.
This guide dissects the Table (com.realmacsoftware.table), Tabs (com.realmacsoftware.tabs), and Gallery (com.realmacsoftware.gallery) components from the open-source Core Pack. Open their source alongside this page.
Detecting the Mode
Everything in this guide starts from one question: am I on the canvas right now? You can answer it in both places your code runs.
In hooks.js, read rw.project.mode — it's "edit" on the canvas and "preview" otherwise. Every core component that cares about mode computes a boolean once, near the top of its hook, and reuses it everywhere. Here is Table's version:
// com.realmacsoftware.table/hooks.source.js
const transformHook = (rw) => {
// …
const { mode } = rw.project;
const edit = mode === "edit";
// …
rw.setProps({
// …
edit,
});
};
exports.transformHook = transformHook;In templates, you don't even need the hook: Elements provides built-in edit and preview variables, so @if(edit), @if(!edit), and the three-way split all work out of the box:
Remember that template @if takes exactly one condition — no &&, ||, or comparisons. When a decision involves mode and something else (Gallery's lightbox below is a good example), combine them in hooks.js and pass a single boolean to the template. See Combining Conditions.
Empty-State Placeholders
The very first thing a user sees after dropping your component on the canvas is your empty state. If that's a zero-height blank box, they'll assume the component is broken. Gallery handles this by computing a single hasResources boolean in its hook:
…and branching its entire main template on it. When the gallery is empty, users don't see nothing — they see a generously sized, dashed-border drop target that tells them exactly what to do next:
Three details worth stealing:
The placeholder is keyed to content, not mode. It appears whenever the gallery is empty, so a user who previews before adding images still gets guidance instead of a void.
The placeholder is functional. The
rwResourceDropZone="resources"attribute (visible in the Gallery source) makes the placeholder itself accept the drop, so the instruction and the target are the same element.It names both paths. Drag and drop or the inspector — whichever workflow the user prefers, the placeholder covers it.
For collection-driven components, the equivalent pattern is a placeholder item: when the collection is empty, fall back to a hard-coded sample entry so the component still renders its real layout. The core Audio Playlist does exactly this with a fallback "first track" — see the full example in Data Collections in hooks.js rather than duplicating it here.
Preview Datasets
Placeholders cover "no content yet". Data-driven components have a harder problem: the real content lives somewhere the editor can't reach. Table's CSV mode is the canonical case — on the published site the data comes from a CSV file or remote URL, fetched and parsed by PHP at request time. None of that machinery exists on the canvas, so in edit mode Table fabricates a realistic sample dataset in its hook instead:
The template's CSV branch then splits on edit: the canvas renders the sample rows through the same classes the real table will use, followed by an honest caption; everything after @else is the live PHP + Alpine implementation that only ever runs on the published site.
Two rules make a preview dataset trustworthy rather than confusing:
Label it. The dimmed "Example data" caption tells users this isn't their CSV, and where the real data will come from.
Style it for real. The sample cells run through the user's actual column configuration — widths, alignment, hidden columns, custom classes — so styling decisions made against fake data survive contact with real data.
Editor-Only CSS
Sometimes the canvas needs styling the published page must never see. Any .css file at the root of templates/ runs through the template engine (see CSS Templates), which means it can use @if(edit) like any other template. Wrap the whole file in it and the file contributes zero bytes to the published page. Tabs uses exactly this to show only the panel currently being edited:
{{editorActiveTabIndex}} is computed in hooks.js from an inspector property, so the stylesheet re-targets a different panel every time the user picks a different tab to edit — no JavaScript involved, which matters because component scripts don't run on the canvas.
Navbar's templates/editor.css shows the minimal end of the same idea: a single rule fixing the width of [data-rwx-droparea] elements, the drop areas Elements injects into the canvas. Those elements only exist in the editor, so the rule is inert when published — but wrapping editor CSS in @if(edit) is still the better habit, because it removes the text from the published output entirely instead of merely leaving it unmatched.
Gating Heavy Behaviour on the Canvas
The canvas is not a browser tab you control. It re-renders constantly as the user edits, and anything that autoplays, observes, animates, or floats above the page becomes noise at best and a click-trap at worst. The core components are aggressive about switching this machinery off in edit mode.
Gallery's lightbox is the clearest example. Rendering a fixed, full-screen overlay on the canvas would be hostile, so the hook decides whether to include the lightbox template at all — and combines the mode check with a user-facing opt-in switch, precisely because template @if couldn't express that combination itself:
The template consumes the single boolean: @includeIf(includeLightbox, template: "lightbox"). By default the lightbox simply doesn't exist on the canvas; flip the Lightbox Preview switch in the inspector and it appears, so users can still style it. Gate by default, offer an opt-in is the pattern to copy for any overlay, animation, or effect a user might occasionally need to see while editing.
Modal shows the template-level counterpart with x-cloak. Alpine removes x-cloak attributes when it initialises — but Alpine never initialises on the canvas, so an unconditionally cloaked element would be invisible there forever. Modal only cloaks outside the editor:
On the published page, x-cloak prevents a flash of un-styled modal before Alpine wakes up; on the canvas, omitting it keeps the dialog markup renderable so its content stays editable (more on that below). Table applies the same discipline to plain interactivity: its wrapper only gets x-data="elementsTable(…)" outside edit mode, and the search input renders disabled on the canvas — visible, styleable, but inert.
Keeping Hidden Content Editable
Show/hide components have a built-in edit-mode conflict: at runtime, most of their content is supposed to be invisible, but on the canvas every dropzone must stay reachable or users can't fill it. The core answer is to run visibility through two completely separate mechanisms — Alpine at runtime, precomputed inline styles in the editor.
Tabs decides which panel is "active" from a different source per mode: the visitor-facing defaultActiveTab property at runtime, but a dedicated editorActiveTab inspector property on the canvas, so users choose which tab they're editing. The hook folds that into a per-panel boolean:
The panel markup then carries both mechanisms side by side — Alpine bindings that only exist outside edit mode, and a plain display: none that only appears in the editor:
Every panel is always in the markup; which one is visible is an editor decision on the canvas and an Alpine decision at runtime. Accordion is the same idea inverted: its collapsible region starts display: none on the published page (Alpine's x-collapse opens it), but in edit mode the inline style is skipped so the panel sits open and its dropzone is always reachable:
Modal rounds out the pattern in its hook: the dialog's classes include hidden on the canvas unless the user enables a show-in-edit switch, and the full-screen overlay always gets pointer-events-none in edit mode so it can never swallow canvas clicks even while visible.
Ship-quality checklist — run through this before releasing a component:
Related Documentation
rw.project — the
modeproperty and other project data@if — built-in
edit/previewvariables and combining conditionsCSS Templates — how
templates/*.cssfiles are processedData Collections in hooks.js — placeholder items for empty collections
Last updated
Was this helpful?

