DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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))
);
Enter fullscreen mode Exit fullscreen mode

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]); }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)