Snip
Browser code editor built on Monaco. Real file tree on disk via Node API routes, drag-and-drop reordering, autosave, VS Code–style tabs, and a Run panel that traces JS errors back to the source line.
Local-first browser code editor
- TypeScript
- Next.js
- Monaco
- React
- dnd-kit
Why I built this
I wanted a fast scratchpad that wasn’t CodePen. Something I could run locally with a Monaco editor, that remembered my files between sessions, and that could run JavaScript without me setting up a project for it.
CodeSandbox is too heavy for the kind of “let me just try this”
workflow I have. A single index.html is too light. Snip was
meant to sit in the middle. A browser-based editor with a real
keyboard feel and not much else.
What it does
- File explorer with create, rename, delete via right-click, and drag-and-drop reordering.
- Tabs and editor powered by Monaco. Auto-save on edit (debounced) with syntax highlighting.
- A Run panel that executes JavaScript and shows
console.logoutput, runtime errors, and the line number the error came from. - Files live as files on disk. There’s no database. Next.js API routes write to the filesystem.
- The active folder and tab survive a reload.
Architecture
It’s one Next.js project. The client is a Monaco-driven shell.
The server is a thin REST layer over fs. No custom server, no
websockets, no DB.
Technical decisions
Monaco over CodeMirror. Monaco is bigger, but you get VS Code’s keymap, IntelliSense, and theming for free. The keyboard experience is most of the product so the size was worth it.
dnd-kit for the file tree. Lighter than react-dnd and the
sensors (pointer, keyboard, touch) all work the same way. Tree
operations don’t need conditional code per input type.
Files on disk, no DB. A real database would have made multi-user trivial and single-user worse. Local-first was the priority so the filesystem is the store.
MUI for chrome. Modal, context menu, status bar. MUI’s primitives are good enough that I didn’t have to build a UI shell.
Lodash debounce on save. Auto-saving on every keystroke thrashes the disk. 250ms is invisible to a person and friendly to the file watcher.
import { debounce } from "lodash";
const persist = debounce(async (path: string, contents: string) => {
await fetch("/api/files", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ path, contents }),
});
}, 250);
monacoEditor.onDidChangeModelContent(() => {
persist(activePath, monacoEditor.getValue());
});
What I learned
Monaco is a 4 MB dependency that was worth it. Multi-cursor, command palette, surround, rename. Rebuilding any of those in a smaller editor would have taken a long time.
Drag-and-drop tree UX has more detail than I expected. Insertion indicators, expand-on-hover, scroll-near-edge. Each one is its own little feature. dnd-kit lets you build them without rewriting the dragging core.
“No database” pushes you toward a small data model where every operation maps to a file mutation. That ended up being easier to reason about than I thought.
Next steps
- Multi-language Run panel. Python via Pyodide, TypeScript via esbuild-wasm, Rust via the WASM toolchain.
- Workspace export to
.zipand import back. - A read-only share link generated as a snapshot. Not live collab, just a frozen copy.
- Pull the Run panel out into a standalone component so I can use it in blog posts.
Source: github.com/uzairali19/snip.