Fixing cmdk scroll inside a Dialog. The one prop nobody tries first.

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

You have a combobox using cmdk (shadcn Command component) inside a Radix Dialog. The list has more items than fit. You expect it to scroll. It does not scroll.
You are not alone. This issue has hundreds of comments across GitHub. People try everything. Most solutions are hacks. The real fix is one prop.
cmdk renders a CommandList with a max height and overflow-y: auto. In isolation it scrolls fine. Put it inside a Radix Dialog and the scroll breaks.
The reason is Radix Popover uses a Portal by default. The portal renders the popover content outside the Dialog DOM tree. Radix Dialog applies scroll locking to the body. The portal content inherits that scroll lock. Your list cannot scroll.
<CommandList className="max-h-[300px] overflow-y-auto">
Does not work. cmdk internally sets --cmdk-list-height and sometimes overrides your max-height with inline styles. Tailwind classes lose to inline styles.
// Tailwind v4 syntax.
<CommandList className="max-h-[300px]! overflow-y-auto">
Sometimes works. Sometimes cmdk re-applies its inline styles after render and wins again. Unreliable.
<CommandList style={{ maxHeight: '300px', overflowY: 'auto' }}>
cmdk uses a ResizeObserver that sets --cmdk-list-height on the list element. Depending on render order your inline style might get overwritten. Flaky.
// In your popover.tsx component file.
// Replace PopoverPrimitive.Portal with a fragment.
<>{content}</>
This actually fixes the scroll. But it breaks positioning. The popover no longer renders in a portal so it clips inside parent containers with overflow: hidden. You trade one bug for another.
<ScrollArea className="h-48 overflow-auto">
<CommandEmpty>Not found</CommandEmpty>
<CommandGroup>
{items.map((item) => (
<CommandItem key={item.value}>{item.label}</CommandItem>
))}
</CommandGroup>
</ScrollArea>
Works in some cases. Adds another scroll container. Can cause double scrollbar issues. Keyboard navigation might not work properly because cmdk expects its own list structure.
<PopoverContent
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
Treats the symptom. The scroll lock is still there. You are just preventing the event from reaching the lock handler. Feels wrong.
<Popover modal>
That is it. Add modal (or modal={true}) to your Popover component.
<Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild>
<Button variant="outline">Select field...</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0">
<Command>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandGroup heading="Fields">
{fields.map((field) => (
<CommandItem key={field.name}>{field.label}</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
No ScrollArea. No inline styles. No !important. No removing portals. No wheel event hacks.
When modal={true} Radix Popover takes over scroll management itself. It tells the Dialog scroll lock to back off for this popover. The popover content is treated as its own modal context. The body scroll lock no longer applies to the content inside the popover.
The CommandList max height from your command.tsx component (max-h-[300px] overflow-y-auto) works as intended. cmdk does not fight it. The list scrolls.
If you are not inside a Dialog at all and scroll still breaks. Check if your parent container has overflow: hidden. The popover portal renders at the document root but the content might still be constrained by CSS. In that case avoidCollisions and collisionPadding on PopoverContent help.
<PopoverContent
avoidCollisions
collisionPadding={16}
>
The scroll issue is caused by Radix Dialog scroll locking conflicting with Radix Popover portals. Setting modal on the Popover tells Radix to handle the conflict correctly. Every other fix is a workaround for not setting this one prop.