SuperAPI AI Directory

shadcn-errors-cmdk-version-drift

General & other

View repository

Updated 10-15 22:18

About this skill

shadcn + cmdk Version Drift : Filter Signature, Value Case, Vaul Focus, and the Hidden Wrapper Coupling

The shadcn Command primitive is a thin React wrapper around the third-party cmdk library (https://github.com/pacocoursey/cmdk). The wrapper is copied into your repo at components/ui/command.tsx by shadcn add command. The wrapper is generated against the cmdk version the registry was built against at that moment. Your package.json then carries its OWN copy of cmdk under dependencies. Those two pins can drift apart, and when they do the symptoms are always one of three : items will not filter, items will not click, or CommandItem rendered outside CommandList crashes with TypeError : undefined is not iterable (cannot read property Symbol(Symbol.iterator)).

This skill documents the six drift cases that surface as three of the top-15 issues in shadcn-ui/ui by reaction count (#2944, #2980, #3051) and the fix workflow.

Companion skills :

  • shadcn-syntax-command (Batch B5) : Command composition surface, the nine subcomponents, shouldFilter, async filtering, Cmd+K hotkey
  • shadcn-syntax-drawer (Batch B5) : Vaul Drawer composition and the onOpenAutoFocus / onCloseAutoFocus focus contract
  • shadcn-errors-radix-controlled : the parallel controlled-state contract for Radix-wrapped primitives (Dialog / Sheet / Popover)
  • shadcn-errors-cli-sync-mismatch : the shadcn add ... --overwrite workflow used to repair the wrapper when cmdk is bumped

Quick Reference : The Three Documented Drift Cases (TL;DR)

# Symptom Root cause Fix
1 Custom filter={(value, search) => ...} works but per-item keywords is silently ignored cmdk >= 1.0.0 filter signature is (value, search, keywords) => number. Old 2-arg form still type-checks because keywords is just an unused trailing parameter Rewrite filter to 3-arg form. Use keywords to extend the search corpus. See examples.md §1
2 "Apple" in data-value never matches search="apple" with a custom filter Default cmdk filter uses command-score which lowercases internally via formatInput. A custom filter receives raw value and raw search and must lowercase BOTH or use .toLowerCase().includes(...) Lowercase in the custom filter or rely on the default. See examples.md §2
3 Inside a Vaul Drawer the Command input loses focus on the first keystroke Vaul focus-traps on open and re-focuses its content root. cmdk auto-focuses the CommandInput. They fight, vaul wins on the next tick, the input loses focus Pass onOpenAutoFocus={(e) => e.preventDefault()} to <DrawerContent> so vaul stops fighting cmdk. See examples.md §3

Three additional drift cases compound the above :

# Symptom Root cause Fix
4 <Command shouldFilter={false}> shows every item on every keystroke shouldFilter={false} disables cmdk's internal filter ENTIRELY. You opted out of filtering and never wrote your own Filter the items array yourself in the parent and pass only the filtered subset as children. See examples.md §4
5 import { CommandLoading } from "@/components/ui/command" fails with "CommandLoading is not exported" The shadcn registry wrapper components/ui/command.tsx re-exports nine names (Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut). CommandLoading is exported by cmdk but NOT by the shadcn wrapper Import directly : import { Command as CommandPrimitive } from "cmdk"; const CommandLoading = CommandPrimitive.Loading. See examples.md §5
6 After pnpm up cmdk the Command items become unclickable or throw "Symbol.iterator" TypeError The wrapper in components/ui/command.tsx was generated against a different cmdk version. Bumping cmdk floated the wrapper out of contract. Specifically cmdk 1.0.0 made CommandList a REQUIRED parent of every CommandItem Re-run pnpm dlx shadcn@latest add command --overwrite to regenerate the wrapper against the current cmdk pin, OR roll cmdk back to the previously-pinned version. See examples.md §6

Quick Reference : Detection Commands

Three commands answer "is there drift in this repo right now" :

# 1. What cmdk version is installed?
pnpm ls cmdk
# or : npm ls cmdk
# or : yarn list cmdk

# 2. What cmdk version does the shadcn registry expect right now?
curl -s https://ui.shadcn.com/r/styles/new-york/command.json \
  | jq '.dependencies'
# Look for "cmdk" : "<version>" in the dependencies array

# 3. Does the wrapper file reference any cmdk API the installed version lacks?
grep -E "CommandLoading|keywords=" components/ui/command.tsx
# If the wrapper imports CommandLoading but cmdk is <1.0.0, drift.
# If the wrapper does NOT pass keywords through but you use it, drift.

The expected outputs match like this :

pnpm ls cmdk says Registry expects Drift verdict
cmdk 0.2.1 cmdk ^1.0.0 DRIFT : wrapper expects 1.0.0 API on a 0.2.x install
cmdk 1.0.4 cmdk ^1.0.0 OK : same major
cmdk 1.1.0 cmdk ^1.0.0 OK if minor is backward-compatible. Verify against cmdk CHANGELOG
cmdk 0.2.1 cmdk ^0.2.0 OK : both pre-1.0
cmdk not installed any BROKEN : shadcn add command never ran. Re-run it

Quick Reference : Fix Strategies

There are three valid fix paths. Pick exactly one per project and document it in your README so future contributors do not re-introduce drift :

Strategy A : Pin cmdk to the registry's exact version

When you want the wrapper file to remain untouched (e.g. you have local customisations on top of the shadcn wrapper) :

// package.json
{
  "dependencies" : {
    "cmdk" : "1.0.4"
  }
}

NEVER use ^ or ~ on cmdk if you are pinning. The carrot is what caused the drift in the first place. Verified at https://github.com/shadcn-ui/ui/issues/2944#issuecomment-... (cmdk 1.0.0 was a breaking change that float-versioning silently auto-pulled).

Strategy B : Re-run shadcn add command --overwrite

When you want the wrapper to match the registry's CURRENT pin :

pnpm dlx shadcn@latest add command --diff       # show what will change
pnpm dlx shadcn@latest add command --overwrite  # apply
pnpm install                                    # pull the new cmdk pin

This is the "make-the-wrapper-canonical" path. Loss of local customisation is a real risk ; use Strategy A instead if you have edits to preserve. See shadcn-errors-cli-sync-mismatch for the full --diff / --overwrite discipline.

Strategy C : Upgrade both, migrate the filter call sites

When the cmdk major bump introduces useful features (the 1.0.0 keywords Item prop is a real productivity win for fuzzy search) :

pnpm up cmdk@latest
pnpm dlx shadcn@latest add command --overwrite
# Now grep for any custom filter prop and rewrite to 3-arg :
grep -rn 'filter=' src/ components/

Then rewrite every (value, search) => number filter to (value, search, keywords) => number and decide whether to actually USE the keywords argument (it concatenates the per-item keywords: string[] prop into the matching corpus).

Quick Reference : The Filter Prop Signature Drift In Detail

Verified at https://github.com/pacocoursey/cmdk (README), PR #158 (2024-03-07) and release notes 1.0.0 (2024-03-08) :

// cmdk 0.2.x signature :
type CommandFilter = (value : string, search : string) => number
// returns 1 = match, 0 = no match (or a fuzzy score in (0, 1])

// cmdk 1.0.0+ signature :
type CommandFilter = (
  value : string,
  search : string,
  keywords ?: string[]
) => number
// keywords are the per-Item `keywords` prop array, undefined if unset

The drift is silent because TypeScript happily accepts a 2-arg function where a 3-arg type is expected (extra parameters are allowed by contravariance). The compiler does not warn. The runtime simply never sees your handling of keywords, so per-item keywords arrays are ignored even when you set them on every CommandItem.

The DEFAULT cmdk filter (when you do not pass filter at all) is :

import commandScore from "command-score"
const defaultFilter : CommandFilter =
  (value, search, keywords) => commandScore(value, search, keywords)

command-score is case-insensitive because it calls toLowerCase() on both the haystack and the needle inside its formatInput helper. Verified at https://raw.githubusercontent.com/pacocoursey/cmdk/main/cmdk/src/command-score.ts (2026-05-19).

If you write a custom filter, you get the raw value and raw search with their case intact. Case-insensitive matching is YOUR job in a custom filter.

Quick Reference : Value Normalization Contract

cmdk stores each item's value on a data-value attribute on the <div> rendered by CommandItem. The value comes from one of :

  1. The explicit value prop on CommandItem (preferred ; stable across re-renders).
  2. The text content of the item (fallback ; volatile if the children change).

Before storage, cmdk applies .trim() (verified, README quote : "Values are always trimmed with the trim() method") but NOT .toLowerCase(). The data-value attribute therefore preserves case.

The DEFAULT filter (command-score) is case-insensitive because the algorithm lowercases internally. A CUSTOM filter is case-sensitive unless you lowercase both arguments yourself.

This is the source of "Apple does not match apple" reports : a custom filter that does value === search or value.includes(search) against the raw arguments returns 0 (= no match) when the case differs.

Quick Reference : The Vaul Drawer + Command Focus Duel

Vaul (https://github.com/emilkowalski/vaul) ships its own focus trap. When <DrawerContent> opens, vaul calls focus() on the content root, which is the element wrapping your <Command>. cmdk's <CommandInput> also has autoFocus semantics : it ensures the input is focused on mount so keyboard typing works without an extra click.

Without intervention :

  1. Drawer opens. Vaul focuses DrawerContent root.
  2. Command mounts. cmdk focuses the inner <input>.
  3. React commits the focus to the input.
  4. Vaul's effect runs on the NEXT tick and re-focuses the root.
  5. The user's first keystroke goes nowhere.

The fix is to tell vaul to stop fighting :

<DrawerContent
  onOpenAutoFocus={(e) => e.preventDefault()}
>
  <Command>
    <CommandInput placeholder="Type a command..." />
    <CommandList>...</CommandList>
  </Command>
</DrawerContent>

onOpenAutoFocus is a Vaul Radix-derived prop. Calling e.preventDefault() disables the default focus-trap entry behaviour. cmdk then wins and the input stays focused.

The same duel happens between cmdk and Radix Dialog when Command is inside a Dialog. Same fix : <DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>.

Quick Reference : shouldFilter and Async Search

shouldFilter defaults to true. cmdk filters and sorts the rendered items by command-score against the current search.

shouldFilter={false} disables BOTH filtering AND sorting. cmdk renders every CommandItem you give it, in the order you give them. This is the correct setting when results come from a server (debounced fetch on input change) ; cmdk should NOT also try to fuzzy-filter them.

The trap : developers flip shouldFilter={false} and then forget to filter the items in their parent state. Result : the list shows the entire result set on every keystroke, ignoring what the user typed.

The contract :

shouldFilter Who filters Who sorts
true (default) cmdk (command-score) cmdk (descending score)
false YOU (in parent state, before passing items) YOU

When shouldFilter={false}, ALWAYS pair it with a CommandLoading indicator (imported from cmdk directly, NOT from the shadcn wrapper) and a debounced fetch. See examples.md §4.

Quick Reference : CommandLoading Is Not Re-Exported

The shadcn registry wrapper at components/ui/command.tsx re-exports exactly these names :

// components/ui/command.tsx (shadcn registry, verified 2026-05-19)
export {
  Command,
  CommandDialog,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandGroup,
  CommandItem,
  CommandSeparator,
  CommandShortcut,
}

CommandLoading is exported by cmdk itself (as Command.Loading) but NOT re-exported by the shadcn wrapper. Importing it from @/components/ui/command fails with "CommandLoading is not exported".

Two valid imports :

// Path A : import from cmdk directly
import { Command as CommandPrimitive } from "cmdk"
const CommandLoading = CommandPrimitive.Loading
// Path B : extend the shadcn wrapper to re-export it
// Edit components/ui/command.tsx :
import { Command as CommandPrimitive } from "cmdk"
// ...existing code...
const CommandLoading = CommandPrimitive.Loading
export { /* existing exports */, CommandLoading }

Path B makes the wrapper non-re-add-safe : the next shadcn add command --overwrite will wipe your re-export. Document it in your README or use Path A.

Quick Reference : The CommandItem-Outside-CommandList Crash

cmdk 1.0.0 changed the internal item registry from a flat map to a list-scoped map. Every CommandItem MUST now be a descendant of CommandList. Rendering an item outside crashes with :

TypeError : undefined is not iterable (cannot read property
Symbol(Symbol.iterator))

This is the same crash reported in issue #2944. The fix is structural, not a prop : wrap every CommandItem in a CommandList. Example correct shape :

<Command>
  <CommandInput placeholder="Search..." />
  <CommandList>
    <CommandEmpty>No results.</CommandEmpty>
    <CommandGroup heading="Suggestions">
      <CommandItem value="calendar">Calendar</CommandItem>
      <CommandItem value="search">Search</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

CommandEmpty must also live INSIDE CommandList for the same registry reason.

See Also

  • references/methods.md : filter signatures per cmdk version, full compatibility matrix, shadcn wrapper vs cmdk dependency tree
  • references/examples.md : six side-by-side WRONG vs RIGHT examples covering each drift case
  • references/anti-patterns.md : the six drift anti-patterns with root cause and fix for each