Documentation
Mantle adds a visual editing layer to your existing application. Install an SDK, connect your project, and start editing content directly in your browser.
Getting Started
Mantle provides SDK packages for different frameworks. The core package @mantle/sdk is framework-agnostic. For React and Next.js projects, use @mantle/react.
Installation
npm install @mantle/reactOr if you're using the core SDK directly:
npm install @mantle/sdkCore Concepts
Mantle works by mapping editable regions in your UI to content identifiers. These mappings are defined through either React components (with @mantle/react) or HTML data attributes (with @mantle/sdk).
In edit mode, Mantle overlays editing controls on your running application. In production, the SDK adds zero overhead — components render as plain HTML elements.
React Integration
The @mantle/react package provides React components and hooks for editable content.
MantleProvider
Wrap your app (or any subtree) with MantleProvider to enable visual editing.
import { MantleProvider } from '@mantle/react'
export default function Layout({ children }) {
return (
<MantleProvider>
{children}
</MantleProvider>
)
}MantleText
Makes text content editable. Renders as a span by default. Supports rich text editing when connected to the editor.
import { MantleText } from '@mantle/react'
function Hero() {
return (
<h1>
<MantleText id="hero-title">
Welcome to our site
</MantleText>
</h1>
)
}MantleImage
Makes images swappable through the visual editor.
import { MantleImage } from '@mantle/react'
function Hero() {
return (
<MantleImage
id="hero-image"
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={600}
/>
)
}MantleSection
Wraps a content section to enable reordering and section-level operations in the editor.
import { MantleSection } from '@mantle/react'
function FeaturesSection() {
return (
<MantleSection id="features" label="Features">
<div className="grid grid-cols-3 gap-8">
{/* Feature cards */}
</div>
</MantleSection>
)
}useMantle Hook
For more control, the useMantle hook gives you direct access to editable values.
import { useMantle } from '@mantle/react'
function PricingCard() {
const price = useMantle('pricing-amount', '$29/mo')
const label = useMantle('pricing-label', 'Pro Plan')
return (
<div>
<h3>{label}</h3>
<p>{price}</p>
</div>
)
}Content Binding
If you're using the core SDK or prefer HTML attributes over React components, you can bind content with data attributes.
| Attribute | Purpose |
|---|---|
data-acms-id | Unique identifier for the editable element |
data-acms-type | Content type: text, rich-text, image, link |
data-acms-label | Human-readable label shown in the editor sidebar |
data-acms-element | Maps root element to its element definition |
<h1
data-acms-id="hero-title"
data-acms-type="text"
data-acms-label="Hero Title"
>
Welcome to our site
</h1>
<img
data-acms-id="hero-image"
data-acms-type="image"
data-acms-label="Hero Image"
src="/hero.jpg"
alt="Hero"
/>Connected Mode
Connected mode lets Mantle communicate with your running dev server in real time. When connected, edits in the Mantle editor are reflected immediately in your app.
Setup
- Start your dev server as usual (e.g.
npm run dev) - In your Mantle project settings, set the Dev Server URL to your local address (e.g.
http://localhost:3000) - Enable Connected Mode in project settings
- Open the editor — Mantle loads your dev server in the editing iframe and communicates changes via postMessage
Project Configuration
Connected projects have these additional fields:
{
"framework": "nextjs", // or "what", "vite", "other"
"devServerUrl": "http://localhost:3000",
"sourceDirectory": "./src",
"isConnected": true
}What Framework Integration
The @mantle/what package provides deep integration with the What Framework, including a Vite plugin and a signal-based adapter.
Installation
npm install @mantle/whatVite Plugin
The Vite plugin auto-injects the Mantle SDK in development mode and hooks into What Framework's signal registry for live editing.
import { defineConfig } from 'vite'
import { mantlePlugin } from '@mantle/what/vite'
export default defineConfig({
plugins: [
mantlePlugin()
]
})Component Authoring
What Framework components use signal() for reactive state. To make content editable with Mantle, signal variable names must match their corresponding data-acms-id values.
Component Structure
A Mantle-compatible What component follows this pattern: declare signals for editable content, then reference them in JSX with the matching data-acms-* attributes.
import { signal } from 'what-core'
export function HeroSection() {
const headline = signal('Build Something Amazing')
const subtitle = signal('Ship faster with visual editing')
return (
<section data-acms-element="hero-section" class="py-20 px-4">
<h1
data-acms-id="headline"
data-acms-type="text"
data-acms-label="Headline"
>
{headline()}
</h1>
<p
data-acms-id="subtitle"
data-acms-type="text"
data-acms-label="Subtitle"
>
{subtitle()}
</p>
</section>
)
}Required Attributes
| Attribute | Required On | Purpose |
|---|---|---|
data-acms-element | Root element | Maps component to its element definition (use the component slug) |
data-acms-id | Each editable node | Must match the signal variable name exactly |
data-acms-type | Each editable node | Content type: text, rich-text, image |
data-acms-label | Each editable node | Human-readable label for the editor sidebar |
Signal Naming Rules
The signal variable name is how Mantle maps editor changes back to your source code. The adapter supports several naming conventions:
- Exact match (highest priority):
const headline = signal(...)→data-acms-id="headline" - Dot notation:
const heroTitle = signal(...)→data-acms-id="hero.title" - Underscore notation:
const hero_title = signal(...)→data-acms-id="hero.title"
Control Flow
What Framework uses <Show>, <For>, and <Switch> instead of ternaries and .map(). Components run once — only signal-reading nodes re-render.
import { signal } from 'what-core'
import { For, Show } from 'what-core'
export function FeatureList() {
const title = signal('Features')
const showBadge = signal(true)
const features = signal([
{ name: 'Visual Editor', desc: 'Edit content in the browser' },
{ name: 'AI Generation', desc: 'Generate sections with AI' },
])
return (
<section data-acms-element="feature-list" class="py-16">
<h2 data-acms-id="title" data-acms-type="text" data-acms-label="Title">
{title()}
</h2>
<Show when={showBadge()}>
<span class="badge">New</span>
</Show>
<div class="grid grid-cols-2 gap-6">
<For each={features()}>
{(feature) => (
<div class="p-4 border rounded-lg">
<h3>{feature.name}</h3>
<p>{feature.desc}</p>
</div>
)}
</For>
</div>
</section>
)
}JSX Differences from React
- Use
classinstead ofclassName - HTML attributes use their native names (
for,tabindex, etc.) - Signal values are accessed by calling the signal:
{count()}not{count} - Event handlers use native names:
onclicknotonClick
DevTools Integration
What Framework exposes a global window.__WHAT_DEVTOOLS__ object in development mode. Mantle's adapter hooks into this to discover signals, track components, and map editable content to live state.
Signal Registry
The DevTools global provides a registries.signals map containing every signal in the running app. Each entry has:
{
id: number, // Unique signal ID
name: string, // Variable name (e.g. "headline")
value: unknown, // Current value
componentId?: number // ID of owning component (for scoping)
}How Mantle Uses DevTools
- Signal discovery: On
acms:init, the adapter reads all registered signals and builds a map fromdata-acms-idvalues to signal IDs. - Live updates: When the editor sends a content change, the adapter calls
signal.set(newValue)— the framework reactively updates only the affected DOM nodes. - Event subscription: The adapter listens for
signal:createdandsignal:updatedevents to keep its map current as components mount and unmount.
Component Scoping
When two components have signals with the same name (e.g. both have a title signal), the adapter uses componentId scoping to match the correct signal. This prevents false matches in pages with multiple instances of similar components.
Vite Plugin Configuration
The @mantle/what Vite plugin handles SDK injection and adapter setup automatically. Here are the available configuration options:
import { defineConfig } from 'vite'
import { mantlePlugin } from '@mantle/what/vite'
export default defineConfig({
plugins: [
mantlePlugin({
// Mantle project slug (required for connected mode)
projectSlug: 'my-project',
// Mantle editor URL (default: https://usemantle.app)
editorUrl: 'https://usemantle.app',
// Auto-inject SDK script in dev mode (default: true)
inject: true,
// Enable dev overlay with element outlines (default: true)
devOverlay: true,
})
]
})What the Plugin Does
- Script injection: Adds the Mantle SDK
<script>tag to your HTML in development mode. Removed automatically in production builds. - Adapter registration: Registers the What Framework signal adapter so the SDK can read and write signal values.
- DevTools bridge: Connects to
window.__WHAT_DEVTOOLS__for signal discovery and component tracking. - HMR integration: Re-maps signals after hot module replacement to keep editing state consistent.
Production Behavior
In production builds, the plugin is a no-op. No SDK code is bundled, no data attributes are stripped — your components render as-is with zero overhead. The data-acms-* attributes remain in the HTML but are inert without the SDK.
Store Integration
What Framework stores provide shared reactive state across components. Mantle can bind to store properties in addition to signals, enabling editing of shared content like navigation items, theme settings, or global configuration.
Binding to Store Properties
Use dot notation in data-acms-id to reference nested store properties. The adapter resolves store bindings when a matching signal is not found.
import { createStore } from 'what-core'
const siteConfig = createStore('siteConfig', {
title: 'My Website',
tagline: 'Built with What Framework',
logo: '/logo.svg',
})
export function SiteHeader() {
return (
<header data-acms-element="site-header" class="flex items-center p-4">
<img
data-acms-id="siteConfig.logo"
data-acms-type="image"
data-acms-label="Site Logo"
src={siteConfig.logo}
alt="Logo"
/>
<div>
<h1
data-acms-id="siteConfig.title"
data-acms-type="text"
data-acms-label="Site Title"
>
{siteConfig.title}
</h1>
<p
data-acms-id="siteConfig.tagline"
data-acms-type="text"
data-acms-label="Tagline"
>
{siteConfig.tagline}
</p>
</div>
</header>
)
}How Store Editing Works
- The adapter builds a store map from
registries.storesin DevTools - When a
data-acms-idlikesiteConfig.titledoesn't match any signal, the adapter checks store bindings - Edits call
store.set(path, newValue)which reactively updates all components reading that property - When saving to GitHub, the content writer updates the store's default values in your source file
Store vs Signal
Use signals for content scoped to a single component (headings, body text, images). Use stores for content shared across components (site title, navigation, theme). Both are fully editable in the Mantle visual editor.
PostMessage Protocol
The Mantle editor communicates with the preview iframe using postMessage. All messages are prefixed with acms:.
| Message | Direction | Description |
|---|---|---|
acms:init | Editor → Preview | Initialize the SDK in the preview frame |
acms:content-update | Editor → Preview | Push a content change to the preview |
acms:element-selected | Preview → Editor | User clicked an editable element |
acms:ready | Preview → Editor | SDK initialized and ready |
To listen for messages from the editor in your custom integration:
window.addEventListener('message', (event) => {
if (typeof event.data !== 'string') return
if (!event.data.startsWith('acms:')) return
const [, type] = event.data.split(':')
const payload = JSON.parse(event.data.slice(event.data.indexOf('{')) || '{}')
switch (type) {
case 'content-update':
// Apply content change
break
case 'init':
// Initialize SDK
break
}
})Need help? Sign up and reach us through the dashboard.