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

Introduction
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.
The problem.
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.
What people try first.
Adding max-height and overflow classes.
<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.
Using !important.
// 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.
Inline style overrides.
<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.
Removing the Portal from Popover.
// 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.
Wrapping in ScrollArea.
<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.
Stopping wheel event propagation.
<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.
The fix. One prop.
<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.
Why this works.
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.
When you might still need other fixes.
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}
>
Summary.
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.






