Anatomy of the GodUI Command
One highlight that glides with your arrow keys but snaps while you type, and why those two cases need different motion.
↓Scroll to step through it
One highlight
cmdk marks the selected item with data-selected="true" and moves it on
arrow keys and pointer moves. GodUI watches that attribute and moves one
highlight element to the new item with a FLIP. Its box snaps to the new
item, then translate and scale animate back from the old one.
The highlight lives inside the scrolling list, so it scrolls with the items. When cmdk scrolls a selected item into view, the highlight is already there.
Snap while filtering
Typing re-filters the list on every keystroke: items disappear, the rest shift up, and the selection jumps to the first match. If the highlight slid each time, it would chase the list around while you type.
So the shared useActiveIndicator hook slides only when the selection moves
between existing items (an attribute change), and snaps when items are added
or removed. Arrow keys glide; typing doesn't.
The result
Arrow through the items, move the pointer over them, then type to filter.
useActiveIndicator(listRef, indicatorRef, {active: '[cmdk-item][data-selected="true"]',items: "[cmdk-item]",attributes: ["data-selected"],});Anatomy of the GodUI Command
One highlight that glides with your arrow keys but snaps while you type, and why those two cases need different motion.
useActiveIndicator(listRef, indicatorRef, {active: '[cmdk-item][data-selected="true"]',items: "[cmdk-item]",attributes: ["data-selected"],});One highlight
cmdk marks the selected item with data-selected="true" and moves it on
arrow keys and pointer moves. GodUI watches that attribute and moves one
highlight element to the new item with a FLIP. Its box snaps to the new
item, then translate and scale animate back from the old one.
The highlight lives inside the scrolling list, so it scrolls with the items. When cmdk scrolls a selected item into view, the highlight is already there.
const mutations = new MutationObserver((records) => {// Selection moved → slide. Items added/removed (filtering) → snap.if (records.some((record) => record.type === "childList")) { observeItems(); place(false);} else { place(true);}});Snap while filtering
Typing re-filters the list on every keystroke: items disappear, the rest shift up, and the selection jumps to the first match. If the highlight slid each time, it would chase the list around while you type.
So the shared useActiveIndicator hook slides only when the selection moves
between existing items (an attribute change), and snaps when items are added
or removed. Arrow keys glide; typing doesn't.
The result
Arrow through the items, move the pointer over them, then type to filter.
What's animated
The highlight is the same FLIP that Tabs uses. Before JavaScript runs, the selected item paints its own background, exactly as in shadcn.
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Arrow keys, pointer | FLIP (WAAPI) on the highlight | translate, scale | ease-spring-snappy | 260ms |
| Typing (filter) | highlight snaps to the first match | none | none | none |
| Dialog open / close | as Dialog | opacity, scale | snappy / expo | 260 / 150ms |
Why GPU-only
The highlight animates translate and scale. Items no longer repaint their
own background on selection.
Reduced motion
The highlight jumps to the selected item without sliding. CommandDialog
fades without scaling.
Replacing shadcn
npx shadcn add @godui/command overwrites components/ui/command.tsx and
installs the GodUI dialog it uses. Exports, props and data-slot attributes
match shadcn/ui new-york-v4, so existing imports keep working. CommandList
renders one extra element, an aria-hidden <span data-slot="command-indicator">.