VOOZH about

URL: https://dev.to/dev48v/i-rebuilt-notions-slash-menu-in-70-lines-of-javascript-2n9i

⇱ I Rebuilt Notion's Slash Menu (/) in ~70 Lines of JavaScript - DEV Community


Notion's slash menu feels like magic: type /, a command palette appears, you filter it, hit Enter, and your block transforms into a heading or a to-do. I rebuilt it in about 70 lines of vanilla JavaScript — and the "magic" turns out to be a string.startsWith("/") check.

⌨️ Try it live: https://dev48v.infy.uk/design/day9-notion-slash.html

This is Day 9 of my DesignFromZero series — famous UI, rebuilt from scratch.

1. A document is just a list of blocks

The core model is humble: a document is an ordered list of blocks, and each block stores one piece of state — its type.

block.dataset.type = "text"; // "h1", "todo", "quote"... the whole block state

All the visible styling derives from that string. Change the string, the block re-renders as a different thing.

2. The slash is a prefix check

No special editor magic. On every input event, read the block's text; if it starts with /, open the menu and treat the rest as a live query:

block.oninput = () => {
 const t = block.textContent;
 if (t.startsWith("/")) openMenu(t.slice(1)); // query after the slash
 else closeMenu();
};

3. Fuzzy filtering via keywords

Each command has a label and a keyword list, so /h1, /title, and /big all surface "Heading 1":

const shown = COMMANDS.filter(c =>
 c.label.toLowerCase().includes(query) ||
 c.keywords.some(k => k.includes(query))
);

Good keyword sets are what make a palette feel like it reads your mind.

4. Keyboard nav with modulo wraparound

While the menu is open, hijack the block's keydown. Up/Down move a selection index; Enter applies it. The modulo trick wraps the cursor from bottom back to top:

if (e.key === "ArrowDown") sel = (sel + 1) % shown.length;
if (e.key === "ArrowUp") sel = (sel - 1 + shown.length) % shown.length;
if (e.key === "Enter") { e.preventDefault(); apply(shown[sel]); }

preventDefault stops the arrows from also moving the text caret.

5. Applying = swap the type, wipe the query

Selecting a command does two tiny things — set the type (CSS handles the visual transform) and clear the /head text:

function apply(cmd) {
 block.dataset.type = cmd.type; // restyle via CSS
 block.textContent = ""; // remove "/head"
 closeMenu();
 placeCaret(block);
}

The takeaway

The features that feel most "designed" are often the simplest underneath. A slash menu is: blocks as data + a prefix check + fuzzy filter + a type swap. Build it once and you understand half of every modern block editor.

Open the demo and try typing /todo or /code. The "Understand" tab walks each step.