A lightweight SVG editor built with MoonBit and the Luna reactive UI library. Inspired by Excalidraw, Moonlight provides a simple yet powerful interface for creating and editing vector graphics.
Live Demo: https://moonlight.mizchi.workers.dev
Try the editor directly in your browser. No installation required.
- SVG-native: Works directly with SVG elements (rect, circle, ellipse, line, text)
- Multiple modes: Full editor, embeddable widget, and Web Component
- Reactive UI: Built on Luna's signal-based reactivity
- Undo/Redo: Full command history support
- Grid snapping: Optional snap-to-grid for precise alignment
- Connected lines: Lines can connect to element anchor points and follow when moved
- Dark/Light themes: Toggle between dark and light modes
- Import/Export: Save and load SVG files
- MoonBit toolchain
- Node.js 18+
- pnpm
git clone <repository-url>
cd moonlight
pnpm install# Start development server
pnpm dev
# Type checking
moon check
# Format code
moon fmt
# Build for production
pnpm build- Full Editor: http://localhost:5173/
- Embed Mode: http://localhost:5173/embed.html
- Web Component: http://localhost:5173/webcomponent.html
| Tool | Shortcut | Description |
|---|---|---|
| Select | V or 1 |
Select and move elements |
| Rectangle | R or 2 |
Draw rectangles |
| Circle | C or 3 |
Draw circles/ellipses |
| Line | L or 4 |
Draw lines and arrows |
| Text | T or 5 |
Add text elements |
| Shortcut | Action |
|---|---|
Ctrl+Z / Cmd+Z |
Undo |
Ctrl+Shift+Z / Cmd+Shift+Z |
Redo |
Ctrl+Y / Cmd+Y |
Redo (alternative) |
Delete / Backspace |
Delete selected element |
Escape |
Deselect / Cancel operation |
| Shortcut | Action |
|---|---|
Arrow Keys |
Move selected element (5px) |
Shift + Arrow Keys |
Move selected element (1px, precise) |
Ctrl + Arrow Keys |
Move selected element (10px, fast) |
| Shortcut | Action |
|---|---|
Ctrl+D / Cmd+D |
Duplicate selected element |
[ |
Send backward |
] |
Bring forward |
Ctrl+[ / Cmd+[ |
Send to back |
Ctrl+] / Cmd+] |
Bring to front |
| Shortcut | Action |
|---|---|
G |
Toggle grid visibility |
Ctrl+G / Cmd+G |
Toggle grid snapping |
- Click: Select element
- Click + Drag: Move element or draw new shape
- Click on canvas: Deselect all
- Right-click: Context menu (delete, duplicate, layer ordering)
Lines can be connected to other elements:
- Select the Line tool (
L) - Click near an element's anchor point (edges or center)
- Drag to another element's anchor point
- The line will automatically follow when connected elements are moved
The right sidebar shows properties of the selected element:
- Position: X/Y coordinates (editable)
- Size: Width/Height or radius (editable)
- Style: Fill color, stroke color, stroke width
- Line markers: Add arrows to line endpoints
Toggle between light and dark themes using the theme button in the toolbar.
The standalone editor with all features enabled.
<script type="module" src="./main.ts"></script>Embed the editor in your application using JavaScript:
<script type="module" src="./embed.ts"></script>
<div id="editor-container"></div>
<script>
const editor = MoonlightEditor.create(
document.getElementById('editor-container'),
{
width: 800,
height: 600,
theme: 'light', // or 'dark'
readonly: false
}
);
// API
const svg = editor.exportSvg();
editor.importSvg(svgString);
editor.clear();
editor.hasFocus();
</script>Use as a custom HTML element:
<script type="module" src="./webcomponent.ts"></script>
<moonlight-editor
width="800"
height="600"
theme="light">
</moonlight-editor>
<script>
const editor = document.querySelector('moonlight-editor');
// API (same as embed mode)
const svg = editor.exportSvg();
editor.importSvg(svgString);
editor.clear();
editor.hasFocus();
</script>| Attribute | Type | Default | Description |
|---|---|---|---|
width |
number | 400 | Canvas width in pixels |
height |
number | 300 | Canvas height in pixels |
theme |
string | "light" | Theme: "light" or "dark" |
readonly |
boolean | false | Disable editing |
Embed the editor in any HTML page with just 2 lines:
<moonlight-editor width="800" height="600"></moonlight-editor>
<script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script><moonlight-editor width="800" height="500">
<template>
<svg viewBox="0 0 800 500">
<rect x="100" y="100" width="120" height="80" fill="#4CAF50" stroke="#2E7D32" stroke-width="2"/>
<circle cx="400" cy="200" r="50" fill="#2196F3" stroke="#1565C0" stroke-width="2"/>
<text x="300" y="350" font-size="24" fill="#333">Hello Moonlight!</text>
</svg>
</template>
</moonlight-editor>
<script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script><button onclick="alert(editor.exportSvg())">Export SVG</button>
<button onclick="editor.clear()">Clear</button>
<moonlight-editor id="editor" width="800" height="600" theme="dark"></moonlight-editor>
<script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script>
<script>
const editor = document.getElementById('editor');
// editor.exportSvg() - Get SVG string
// editor.importSvg(svg) - Load SVG
// editor.clear() - Clear canvas
// editor.hasFocus() - Check focus state
</script>| Attribute | Default | Description |
|---|---|---|
width |
400 | Canvas width (px) |
height |
300 | Canvas height (px) |
theme |
"light" | "light" or "dark" |
readonly |
- | Add to disable editing |
pnpm build:all # Build component
pnpm deploy # Deploy to WorkersThe worker serves files with CORS headers, enabling cross-origin usage.
Both embed and web component modes return an editor handle with these methods:
interface EditorHandle {
// Export current canvas as SVG string
exportSvg(): string;
// Import SVG string to canvas
importSvg(svg: string): void;
// Clear all elements
clear(): void;
// Check if editor has focus
hasFocus(): boolean;
// Register change callback
onChange(callback: () => void): void;
}src/
├── main.mbt # Full editor entry point
├── ui.mbt # UI components (sidebar, toolbar)
├── render.mbt # SVG rendering
├── core/
│ └── scene.mbt # Editor state management
├── model/
│ ├── types.mbt # Data models (Element, Style, etc.)
│ ├── command.mbt # Undo/Redo commands
│ └── element_ops.mbt # Pure element operations
├── lib/ # Shared library code
├── embed/ # Embed mode entry point
├── webcomponent/ # Web Component entry point
└── preview/ # Preview mode
- MoonBit: Systems programming language that compiles to WebAssembly/JavaScript
- Luna: Signal-based reactive UI library for MoonBit
- Vite: Build tool with hot module replacement
- vite-plugin-moonbit: Vite plugin for MoonBit integration
MIT