iframe-visual-editor
v1.0.0
Published
Framework-agnostic, security-first visual HTML editor running in an iframe sandbox.
Downloads
28
Maintainers
Readme
iframe-visual-editor
Framework-agnostic, security-first visual HTML editor running in an iframe sandbox.
Let end-users visually edit HTML content — change text, swap images, restyle elements, reorder sections — directly in the browser, without writing code. All editing happens inside an iframe sandbox, fully isolated from your host application's DOM.
Features
- ✅ Iframe-sandboxed — edits never touch your host DOM
- ✅ Security-first — DOMPurify sanitization on input & output, CSP nonce support
- ✅ Undo/redo — full HTML snapshot history with configurable depth
- ✅ Clean save — clone-before-cleanup pattern strips all editor artifacts
- ✅ Framework-agnostic — works with vanilla JS, React, Vue, Svelte, Angular
- ✅ TypeScript — full type declarations shipped
- ✅ Zero runtime deps — only
dompurifyas a peer dependency - ✅ Multi-format — ESM, CJS, UMD bundles
Quick Start
npm install iframe-visual-editor dompurify<iframe id="my-editor" style="width:100%;height:600px;border:1px solid #ccc"></iframe>
<script type="module">
import { IframeVisualEditor } from 'iframe-visual-editor';
const editor = new IframeVisualEditor({
target: '#my-editor',
html: '<h1>Hello World</h1><p>Click any element to edit it.</p>',
});
await editor.init();
// Get clean HTML at any time
const { html, css } = await editor.getHTML();
</script>Installation
# npm
npm install iframe-visual-editor dompurify
# yarn
yarn add iframe-visual-editor dompurify
# pnpm
pnpm add iframe-visual-editor dompurifyCDN (UMD)
<script src="https://unpkg.com/dompurify@3/dist/purify.min.js"></script>
<script src="https://unpkg.com/iframe-visual-editor@1/dist/umd/iframe-visual-editor.min.js"></script>
<script>
const { IframeVisualEditor } = window.IframeVisualEditor;
</script>Usage Examples
Vanilla JS
import { IframeVisualEditor } from 'iframe-visual-editor';
const editor = new IframeVisualEditor({
target: document.getElementById('my-iframe'),
html: '<div><h1>Title</h1><p>Content here</p></div>',
css: 'body { font-family: sans-serif; padding: 20px; }',
historyDepth: 30,
});
await editor.init();
editor.on('select', ({ element }) => {
console.log('Selected:', element.tagName);
});
editor.on('change', ({ type, description }) => {
console.log('Changed:', type, description);
});
// Save
const result = await editor.getHTML();
console.log(result.html);
// Cleanup
editor.destroy();React
import { useRef, useEffect, useState } from 'react';
import { IframeVisualEditor } from 'iframe-visual-editor';
function VisualEditor({ html }: { html: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const editorRef = useRef<IframeVisualEditor | null>(null);
useEffect(() => {
if (!iframeRef.current) return;
const editor = new IframeVisualEditor({
target: iframeRef.current,
html,
});
editor.init().then(() => {
editorRef.current = editor;
});
return () => editor.destroy();
}, [html]);
const handleSave = async () => {
const result = await editorRef.current?.getHTML();
console.log(result);
};
return (
<>
<iframe ref={iframeRef} style={{ width: '100%', height: '600px' }} />
<button onClick={handleSave}>Save</button>
</>
);
}Vue 3
<template>
<iframe ref="iframeEl" style="width:100%;height:600px" />
<button @click="save">Save</button>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { IframeVisualEditor } from 'iframe-visual-editor';
const iframeEl = ref<HTMLIFrameElement>();
let editor: IframeVisualEditor;
onMounted(async () => {
editor = new IframeVisualEditor({
target: iframeEl.value!,
html: '<h1>Hello Vue</h1>',
});
await editor.init();
});
onUnmounted(() => editor?.destroy());
const save = async () => {
const result = await editor.getHTML();
console.log(result);
};
</script>Next.js
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('../components/VisualEditor'), {
ssr: false, // IframeVisualEditor requires browser APIs
});
export default function Page() {
return <Editor html="<h1>Hello Next.js</h1>" />;
}API Reference
Constructor
new IframeVisualEditor(config: EditorConfig)| Option | Type | Default | Description |
|--------|------|---------|-------------|
| target | string \| HTMLIFrameElement | required | CSS selector or iframe element |
| html | string | required | HTML content to load |
| css | string | '' | Optional CSS to inject |
| sanitize | boolean \| DOMPurifyConfig | true | Sanitize input HTML |
| cspNonce | string | '' | Nonce for injected <style> tags |
| historyDepth | number | 20 | Max undo steps |
| toolbar | boolean | true | Show floating toolbar |
| editable.text | boolean | true | Allow text editing |
| editable.images | boolean | true | Allow image replacement |
| editable.styles | boolean | true | Allow style changes |
| editable.dragDrop | boolean | true | Allow drag reordering |
| editable.delete | boolean | true | Allow element deletion |
| sandbox.allowForms | boolean | false | Enable allow-forms |
| sandbox.allowPopups | boolean | false | Enable allow-popups |
| sandbox.allowTopNavigation | boolean | false | Enable allow-top-navigation |
Lifecycle
| Method | Returns | Description |
|--------|---------|-------------|
| init() | Promise<void> | Initialize the editor |
| destroy() | void | Remove all listeners, cleanup |
| isDestroyed() | boolean | Check if editor is destroyed |
Content
| Method | Returns | Description |
|--------|---------|-------------|
| getHTML(options?) | Promise<SaveResult> | Get cleaned { html, css } |
| setHTML(html, css?) | Promise<void> | Replace content entirely |
| getSelectedElement() | SerializedElement \| null | Get selected element info |
History
| Method | Returns | Description |
|--------|---------|-------------|
| undo() | boolean | Undo last action |
| redo() | boolean | Redo last undone action |
| getHistoryState() | HistoryState | Get { canUndo, canRedo, depth } |
Editing Commands
| Method | Description |
|--------|-------------|
| updateText(text) | Update selected element's text |
| updateImage(src, alt?) | Update selected image |
| updateStyle(property, value) | Update a CSS property |
| deleteSelected() | Delete the selected element |
| moveSelected('up' \| 'down') | Reorder the selected element |
Events
editor.on('ready', () => {});
editor.on('select', (data: SelectionData) => {});
editor.on('deselect', () => {});
editor.on('change', (data: ChangeData) => {});
editor.on('history', (state: HistoryState) => {});
editor.on('save', (result: SaveResult) => {});
editor.on('error', (error: EditorError) => {});
editor.on('destroy', () => {});
editor.off('change', handler);Hooks
new IframeVisualEditor({
hooks: {
beforeSave: (html, css) => ({ html, css }),
afterSave: (html, css) => {},
onSelect: (element, metadata) => {},
onDeselect: () => {},
},
});Security
Iframe Sandboxing
The iframe always has sandbox="allow-same-origin allow-scripts". Additional permissions (allow-forms, allow-popups, allow-top-navigation) are opt-in via config.
DOMPurify Sanitization
Input HTML is sanitized before injection into the iframe:
<script>,<iframe>,<object>,<embed>,<applet>tags are stripped- All inline event handlers (
onerror,onclick, etc.) are removed javascript:anddata:text/htmlURIs are stripped
Output from getHTML() is re-sanitized by default. Disable with getHTML({ sanitize: false }).
CSP Nonce Support
All injected <style> tags accept a configurable nonce:
new IframeVisualEditor({
cspNonce: 'your-random-nonce',
});Minimum CSP required:
style-src 'nonce-<your-nonce>'; script-src 'none';What's Stripped on Save
The save pipeline clones the iframe document and strips:
- All
[data-editor-ui]elements (toolbars, labels, highlights) - Editor CSS classes (
hover-highlight,selected-highlight, etc.) - Editor attributes (
contenteditable,data-draggable, etc.) - Editor inline styles (
outline,cursor: grab, highz-index) - Empty
class=""andstyle=""attributes
User-applied styles (color, font-size, font-weight, etc.) are always preserved.
Architecture
src/
├── index.ts # Public entry point
├── core/
│ ├── IframeVisualEditor.ts # Main orchestrator class
│ ├── IframeManager.ts # Iframe creation, sandboxing
│ └── EventBus.ts # Typed event emitter
├── editing/
│ ├── ElementSelector.ts # Hover highlight, click-to-select
│ ├── InlineEditor.ts # contenteditable, text/image editing
│ ├── StyleEditor.ts # CSS property manipulation
│ └── DragDropManager.ts # Drag reordering
├── history/
│ └── HistoryManager.ts # Undo/redo with HTML snapshots
├── save/
│ ├── SavePipeline.ts # Orchestrates cleanup flow
│ ├── CloneAndClean.ts # Strips editor artifacts from clone
│ ├── ImagePreserver.ts # Preserves image dimensions
│ └── Sanitizer.ts # DOMPurify wrapper
├── ui/
│ ├── Toolbar.ts # Floating action bar
│ ├── HoverLabel.ts # Tag-name label on hover
│ └── SelectionHighlight.ts # CSS highlight classes
├── types/
│ └── index.ts # TypeScript interfaces
└── utils/
├── dom.ts # DOM helper utilities
└── css.ts # CSS injection helpersBrowser Support
| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 88+ | | Safari | 15+ | | Edge | 90+ |
Development
# Install dependencies
npm install
# Type check
npm run typecheck
# Run unit tests
npm test
# Build ESM/CJS/UMD
npm run build
# Dry-run publish
npm pack --dry-runContributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
Please ensure:
- All tests pass (
npm test) - TypeScript compiles (
npm run typecheck) - New features have test coverage
- Security-sensitive code is reviewed carefully
License
MIT © iframe-visual-editor contributors
