Markasso is a fast, minimal, keyboard-first whiteboard engine that runs entirely in the browser.
Built with vanilla TypeScript and the Canvas 2D API — no framework dependencies. Just you, a canvas, and JavaScript doing exactly what it was invented to do.
While Excalidraw excels at freehand sketching and Draw.io offers comprehensive diagramming, Markasso occupies a different niche: smaller, faster, and requiring no sign-in, cookies, or workspace creation.
Markasso was born from a simple frustration: wanting to sketch a quick diagram shouldn't require:
- Waiting for a heavy application to load
- Dismissing cookie consent banners
- Creating an account for "free" features
- Risking lost work due to forgotten exports
Markasso is the alternative. Open, draw, export, done.
| Tool |
Shortcut |
| Hand |
H / Space |
| Select |
V / 1 |
| Rectangle |
R / 2 |
| Ellipse |
E / 3 |
| Rhombus (Diamond) |
D / 4 |
| Arrow |
A / 5 |
| Pen (freehand) |
P / 6 |
| Text |
T / 7 |
| Line |
L / 8 |
| Eraser |
0 |
| Feature |
Description |
| Infinite canvas |
Pan with middle-click or Alt+drag; zoom with Ctrl+scroll |
| Millimeter grid |
Dot · Line · Graph-paper modes (real mm at 96 DPI) |
| Navigation recovery |
F fits all content into view · Shift+0 resets to origin |
| Dark / Light theme |
Switchable themes with system preference support |
| Feature |
Description |
| Resize handles |
8-handle bounding box on every element type |
| Rotation |
Rotate any element via a handle above the selection box (fully undoable) |
| Endpoint editing |
Drag individual endpoints of lines and arrows independently |
| Shift+click multi-select |
Add or remove elements from selection without a marquee |
| Arrow key nudge |
Move selected elements 1px (or 10px with Shift) |
| Lock elements |
Lock any element to prevent selection, movement, or deletion |
| Feature |
Description |
| Smart arrow links |
Connect arrows to shapes — arrows attach to the border (not center), facing each other |
| Hover preview |
Hover to preview connection points before drawing |
| Cascade delete |
Deleting a shape removes all connected arrows and lines |
| Feature |
Description |
| Groups |
Ctrl+G to group · click selects all members · click again enters group for individual editing |
| Shape labels |
Double-click any rectangle or ellipse to type a label, clipped inside the shape |
| Arrow labels |
Add labels to arrow elements |
| Text scaling |
Dragging a text handle scales the font size, not just the box |
| Double-click to edit |
Open any existing text element for inline editing |
| Feature |
Description |
| Shift constraints |
Rectangle/Ellipse → square/circle · Line/Arrow → 45° snap · Resize → keep aspect ratio |
| Hover highlight |
Elements highlight on hover — always know what you're about to select |
| Eraser hover |
Elements highlight as you drag the eraser over them |
| Feature |
Description |
| Floating glass UI |
Excalidraw-style islands: lock · hand · tools · eraser, top-right import + export; mobile gets a compact bottom-right action bar |
| Properties panel |
Stroke color, fill color, stroke width, stroke style, opacity, roughness, rounded corners; font size + family for text |
| Tool lock |
Lock button keeps the active drawing tool after placing a shape instead of reverting to Select |
| Panel toggle |
\ (backslash) shows/hides all UI panels for a distraction-free canvas |
| Keyboard navigation |
Full Tab-based keyboard navigation through all panels |
| About modal |
Info panel with version, links, and credits |
| Feature |
Description |
| Session persistence |
Auto-saved to localStorage — your work survives page refreshes |
.markasso format |
Save and reload your full scene as a .markasso file (JSON) — images included |
| Image import |
Drag-and-drop, file picker, or Ctrl+V paste; .markasso files can also be dropped directly |
| Mermaid import |
Import .mmd / .mermaid files via drag-and-drop or the toolbar button; paste Mermaid text from the clipboard — graph, flowchart, and sequenceDiagram are converted to native elements |
| Export PNG / SVG |
Download the canvas as a 2× PNG or a clean SVG — bounding-box auto-fit |
| Feature |
Description |
| Undo / Redo |
Full command history with Ctrl+Z / Ctrl+Y or Ctrl+Shift+Z |
| Feature |
Description |
| Zero dependencies |
Browser Canvas 2D API only — no React, no framework, no bundled megabytes |
| Feature |
Markasso |
Excalidraw |
Draw.io |
| Zero dependencies |
✅ |
❌ |
❌ |
| No login required |
✅ |
✅ |
✅ |
| Cookie banners |
None |
Varies |
Yes |
| Bundle size |
Minimal |
Moderate |
Large |
| Infinite canvas |
✅ |
✅ |
✅ |
| Keyboard-first design |
✅ |
Partial |
❌ |
| Pure Canvas 2D |
✅ |
✅ |
SVG-based |
| Offline support |
✅ |
✅ |
✅ |
| Handwritten style |
❌ |
✅ |
❌ |
pnpm dev # Start dev server at http://localhost:5173
pnpm build # Type-check and bundle → dist/
pnpm typecheck # TypeScript validation
pnpm test # Run Vitest unit tests
No .env files. No API keys. No containerization required.
| Key |
Action |
H / Space |
Hand (pan) |
V / 1 |
Select |
R / 2 |
Rectangle |
E / 3 |
Ellipse |
D / 4 |
Rhombus (Diamond) |
A / 5 |
Arrow |
P / 6 |
Pen (freehand) |
T / 7 |
Text |
L / 8 |
Line |
0 |
Eraser |
| Key |
Action |
G |
Toggle grid |
F |
Fit all content into view |
Shift+0 |
Reset viewport to origin |
\ |
Toggle all UI panels |
Scroll |
Pan |
Middle-click drag / Alt+drag |
Pan |
Ctrl+scroll |
Zoom to cursor |
| Key |
Action |
Esc |
Cancel / exit group / deselect |
Delete / Backspace |
Delete selected elements |
Arrow keys |
Nudge selection 1px |
Shift+Arrow |
Nudge selection 10px |
Ctrl+A |
Select all |
Ctrl+D |
Duplicate selection |
Ctrl+G |
Group selected elements |
Ctrl+Shift+G |
Ungroup |
Ctrl+Shift+] |
Bring to front |
Ctrl+Shift+[ |
Send to back |
Ctrl+Z |
Undo |
Ctrl+Y / Ctrl+Shift+Z |
Redo |
Shift+click |
Add / remove from selection |
| Key |
Action |
Shift (while drawing) |
Constrain proportions / snap angle |
Shift (while resizing) |
Lock aspect ratio |
| Double-click on text |
Edit text in place |
| Double-click on rect / ellipse |
Edit shape label |
| Text tool |
Click to place · drag to define area · Enter = commit · Shift+Enter = newline · Esc = cancel |
Markasso follows a Redux-style unidirectional data flow — no mutable state, no event spaghetti, no surprises.
User event
│
▼
Tool (onMouseDown / onMouseMove / onMouseUp)
│ dispatches Command
▼
History.dispatch(command)
│ calls
▼
reducer(Scene, Command) → Scene ← pure function, no side effects
│ notifies
▼
Subscribers (toolbar, properties panel, canvas view)
│
▼
render(ctx, scene, canvas) ← called every requestAnimationFrame
- All coordinates in CSS pixels. The canvas buffer is
clientWidth × devicePixelRatio for sharpness, but all viewport offsetX/Y, element positions, and mouse events live in CSS pixel space.
- Immutable scene. Every
Scene object is never mutated. The reducer returns a new reference or the same reference if nothing changed (enabling cheap equality checks).
- Ephemeral commands (pan, zoom, set-viewport, select, tool change) are excluded from the undo stack.
APPLY_STYLE is the single undoable command that updates both appState defaults and all currently selected elements in one atomic operation.
src/
├── core/
│ ├── scene.ts # Scene interface + factory
│ ├── viewport.ts # Pan/zoom math (screenToWorld, worldToScreen)
│ └── app_state.ts # AppState (activeTool, colors, grid, …)
├── elements/
│ └── element.ts # Discriminated union for all element types
├── commands/
│ └── commands.ts # Full Command discriminated union
├── engine/
│ ├── reducer.ts # Pure (Scene, Command) → Scene
│ └── history.ts # Undo/redo stack + pub/sub
├── io/
│ ├── markasso.ts # .markasso save / load (exportMarkasso, importMarkasso)
│ ├── mermaid.ts # Mermaid parser + layout engine → Markasso elements
│ └── session.ts # localStorage auto-save / restore + quota warning toast
├── rendering/
│ ├── renderer.ts # rAF render loop entry point
│ ├── draw_element.ts # Per-type draw dispatch (with rotation + shape labels)
│ ├── draw_grid.ts # Dot / line / mm graph-paper grids
│ ├── draw_selection.ts # Selection box, resize/rotation/endpoint handles + hit testing
│ └── export.ts # exportPNG / exportSVG (bounding-box auto-fit)
├── tools/
│ ├── tool.ts # Tool interface
│ ├── select_tool.ts # Hit test, marquee, drag-move, resize
│ ├── rectangle_tool.ts
│ ├── ellipse_tool.ts
│ ├── line_tool.ts
│ ├── arrow_tool.ts
│ ├── pen_tool.ts # Freehand with Catmull-Rom smoothing
│ └── text_tool.ts # Invisible overlay textarea + in-place editing
└── ui/
├── canvas_view.ts # DOM event hub → world coords → tool
├── toolbar.ts # Floating islands: tools · undo/zoom · import · export · settings
├── properties_panel.ts # Floating panel, anchored next to the context panel
├── context_panel.ts # Left-side action panel (layer order, style, import image)
├── image_import.ts # File picker, drag-and-drop, paste — images + .markasso
├── settings.ts # Hamburger panel: grid, accent color, version
├── eraser_tool.ts # Eraser: hit-test + delete on drag, sword-slash trail effect
└── shortcuts.ts # Global keyboard map (letters + numeric 1–7, 0 for eraser)
| Mode |
Description |
| Dot |
Subtle dots at configurable world-unit spacing |
| Line |
Horizontal + vertical lines |
| mm |
Three-tier graph paper (1 mm / 5 mm / 10 mm) at physical scale assuming 96 DPI |
| Concern |
Choice |
Rationale |
| Language |
TypeScript 5 |
Exhaustive switch on discriminated unions catches unhandled commands at compile time |
| Build |
Vite |
Zero-config, instant HMR, single-file output |
| Rendering |
Canvas 2D API |
Direct pixel control without virtual DOM overhead |
| Testing |
Vitest |
Runs in Node — no browser needed for reducer/viewport math |
| Dependencies |
None |
crypto.randomUUID(), requestAnimationFrame, ResizeObserver — all native |
See CONTRIBUTORS.md for guidelines.
MIT — © 2026 Alberto Barrago
Draw things. Ship things. Touch grass.