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.
Top comments (0)