Skip to content
animorio
IT
All components

The command menu

Menus, Transitions, PhysicsGSAP, CSS onlyAdvancedCompressed weight: Demo 13 KBGSAP + Flip 37 KBFreeDownload the zip

The ⌘K command palette of a project editor, as smooth as a native app: at rest it is a Search or jump to… pill with its shortcut drawn as keys, and pressed (or with ⌘K, Ctrl K) the pill itself grows into the palette. Typing filters the commands with a fuzzy score, the ones left slide to their places and the letters found light up; the band under the chosen row glides on a spring. A command with more inside opens its own page: the one before sinks back into depth, blurring, and breadcrumbs appear at the top. On the theme page, moving over a theme tries it on the palette at once.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The command menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist-mono@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the command menu of a project editor. At rest a search pill with its shortcut drawn as two little keys;
         pressed (or with Cmd K / Ctrl K) the pill itself grows into the palette. Typing filters the commands, the
         ones left slide to their places, the letters found light up; a command with more inside opens its own page,
         the page before sinking back behind it. The rows are made by the script from the list in script.js -->
    <div class="cmdk" data-cmdk data-autoclic>
      <div class="cmdk__shell" data-shell>
        <button class="cmdk__pill" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="cmdk-dialog" aria-keyshortcuts="Meta+K Control+K" data-pill>
          <svg class="cmdk__lens" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.5 4.5a6 6 0 1 0 0 12a6 6 0 1 0 0-12zM15 15l4.5 4.5"/></svg>
          <span class="cmdk__pilltext" data-pilltext>Search or jump to…</span>
          <span class="cmdk__keys" aria-hidden="true"><kbd class="cmdk__key" data-mod>⌘</kbd><kbd class="cmdk__key">K</kbd></span>
        </button>

        <div class="cmdk__dialog" id="cmdk-dialog" role="dialog" aria-modal="true" aria-label="Command menu" data-dialog hidden>
          <div class="cmdk__head">
            <svg class="cmdk__lens" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.5 4.5a6 6 0 1 0 0 12a6 6 0 1 0 0-12zM15 15l4.5 4.5"/></svg>
            <nav class="cmdk__crumbs" aria-label="Where you are in the menu" data-crumbs></nav>
            <input class="cmdk__input" type="text" role="combobox" aria-expanded="true" aria-autocomplete="list" aria-label="Search commands" autocomplete="off" autocapitalize="off" spellcheck="false" enterkeyhint="go" placeholder="Search or jump to…" data-input>
            <kbd class="cmdk__key cmdk__key--esc" aria-hidden="true">esc</kbd>
          </div>
          <div class="cmdk__body" data-body></div>
          <div class="cmdk__foot" aria-hidden="true">
            <span class="cmdk__tip"><kbd class="cmdk__key cmdk__key--icon"><svg viewBox="0 0 24 24"><path d="M12 18V6M7 11l5-5 5 5"/></svg></kbd><kbd class="cmdk__key cmdk__key--icon"><svg viewBox="0 0 24 24"><path d="M12 6v12M7 13l5 5 5-5"/></svg></kbd>Move</span>
            <span class="cmdk__tip"><kbd class="cmdk__key cmdk__key--icon"><svg viewBox="0 0 24 24"><path d="M19 5v7.5H6M10 8.5l-4 4 4 4"/></svg></kbd>Open</span>
            <span class="cmdk__tip" data-backtip><kbd class="cmdk__key cmdk__key--icon"><svg viewBox="0 0 24 24"><path d="M9 6h11v12H9l-5.5-6z"/></svg></kbd>Back</span>
          </div>
        </div>
      </div>
      <p class="sr" role="status" aria-live="polite" data-live></p>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/Flip.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

25 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The command menu” from Animorio and fit it into my site

What it does

On a white background a white pill with a lens, the grey words Search or jump to… and on the right two little keys, ⌘ and K (Ctrl and K off the Mac). With the mouse over it the pill darkens a touch and the keys rise by a pixel; pressed they go down. Pressing the pill, or ⌘K from anywhere, the pill rises and widens into a panel with less rounded corners and a deeper shadow: its words fade where the field begins, and below the command rows come in one after another, gathered under Suggestions, Navigation, Preferences and Help, each with its icon and on the right its keys (G D, ⌘ B) or an arrow. Typing, the rows that do not fit vanish, the ones left slide up to close the gaps and the letters that match turn violet; the grey band runs to the best command, stretching a little as it travels. The arrows move it, the pointer too, Enter runs it: the palette goes back into the pill and the pill says for a moment what happened, with a tick. Change theme… opens its own page: the list before sinks back, smaller and blurring, while the four themes come forward, and breadcrumbs Home and Theme appear in the field. Moving to Dark, Dusk or Paper the palette changes colours at once; Enter keeps them, Backspace in the empty field goes back and puts the old theme back. Open recent… opens the page of projects. Esc or a click outside closes it.

How it works

One surface

The pill and the palette are the same element: a box with the background, the border and the shadow, whose position, width, height and corner radius GSAP moves from the size of the pill to the size of the panel (expo.out). Inside it sit the pill's button, anchored at the top, and the dialog: the first fades in 0.12 seconds, the second appears right after, with the field in the same place the words were, so the pill seems to have opened. Closing, the same in reverse.

The fuzzy search

Every letter typed must appear in the label, in order. For every possible starting point a score is worked out: one per letter, three more if it touches the one before, four if it starts a word, four if the match starts the label, and a little less for the gaps; the best wins, and its positions become the violet <mark>s. Each command's hidden words (dark, appearance…) find it even when the label does not match, with a lower score.

Rows that slide

Before filtering, Flip takes a picture of the rows and group titles; then the ones that do not fit are hidden and Flip.from slides the rest from their old place to the new one. The rows keep their order, so they only move through the room left free and never overlap; the band goes to the best score. Rows coming back wait half of the others' travel before they appear, and the panel's height is measured from the last row (not from scrollHeight, which also counts rows still moved).

The band on a spring

The band under the chosen row is a single element in the list: its position and its height are two springs (BAND) chasing the row, integrated in steps of 1/240 of a second; the height has a softer spring and grows a little with speed, so the band stretches as it runs and settles on arrival. When it arrives, the loop stops.

Pages in depth

Every page has its own list (role listbox) stacked over the others. Going forward the page before shrinks by DEPTH, rises four pixels, blurs by BLUR pixels and fades, while the new one comes in from a little bigger and blurred and comes into focus; going back the opposite. The field gets the breadcrumbs (real buttons to go back), a new placeholder, and each page's search is kept for when you come back.

Accessible like an app

The field is a combobox with aria-controls on the page's list and aria-activedescendant on the chosen row; the rows are options with aria-selected. The window is a modal dialog: Tab only goes round the breadcrumbs and the field, Esc closes and the focus goes back to the pill. An announcement says how many results there are and what the command did.

Values to change

NameFileDefaultWhat it changes
PILLscript.js300The width of the pill at rest, in pixels.
OPENscript.js0.55The seconds of the morph from the pill to the palette.
DEPTHscript.js0.06How far a page sinks back behind the next one (0.06 = 6% smaller).
BLURscript.js6How blurred the page behind is, in pixels.
SLIDEscript.js0.4The seconds the rows take to slide to their new places while you type.
BANDscript.js[520, 36]The spring of the band under the chosen row: stiffness, and damping.
SHORTCUTscript.js'k'The key that opens it with ⌘ (Mac) or Ctrl.

Try them in the Playground

When to use it

For an app with many actions and pages: an editor, an admin panel, a tool for developers or teams, a product's documentation. The inner pages are for choices with several options (theme, project, language, someone to assign); the commands and groups are changed in the list at the top of the script.

On phones

The box takes the width of the screen minus 32 pixels; the pill stays 300 wide. The row of hints at the bottom and the esc key disappear (a phone has no physical keyboard), the rows stay 42 pixels tall for the finger, the list scrolls with the finger inside the panel without scrolling the page, and a tap outside closes it.

With reduced motion

No morph and no depth: the palette fades in already at its size and the pill disappears at once; the pages swap with a fade, without scale or blur. Filtered rows go straight to their places and the band jumps to the chosen row. Everything else, keys, themes, breadcrumbs, works the same.

Pitfalls

  • With the rows reordered by score, they crossed each other while sliding and for an instant words sat on top of each other: now they keep their order, move only through the freed room, and the best score takes the band instead of the top place.
  • Typing right after opening, the rows were still coming in with their tween and Flip measured them halfway: a title jumped over a row. Before filtering, that entrance is sent to its end.
  • With Flip in absolute mode, the leaving rows kept old offsetTops, the band stopped on the group title instead of the row and Enter ran a hidden command: Flip now works with transforms only.
  • The panel's height measured with scrollHeight also counted rows still moved by Flip's transform and left a gap at the bottom: it is measured from the last visible row.
  • The pill filling the whole surface carried its words into the middle of the incoming list as it grew: now it is anchored at the top, and the list appears only once the pill has gone.
  • Trying themes on the theme page and then going back or closing left the palette in the tried theme: going back and closing put back the one kept.

Libraries

GSAP 3.15.0, Flip (GSAP) 3.15.0, Geist (font variabile) 5.3.0, Geist Mono (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP + Flip 37 KB.

Testing

Tested on 5 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components