Skip to content

feat(ui): make dropdown positioning responsive#7828

Open
delucis wants to merge 4 commits into
decaporg:mainfrom
delucis:chris/responsive-dropdowns
Open

feat(ui): make dropdown positioning responsive#7828
delucis wants to merge 4 commits into
decaporg:mainfrom
delucis:chris/responsive-dropdowns

Conversation

@delucis
Copy link
Copy Markdown
Contributor

@delucis delucis commented May 23, 2026

Summary

This PR continues #7820, #7825, and #7827.

It makes the positioning of dropdown menus responsive by ensuring they remain within the viewport.

For example, here’s a dropdown in the collections UI from #7827 demonstrating how this PR fixes the overflow:

Before After
image image

The implementation works by calculating the dropdown position for the desired left/right placement, and then shifting it along the x-axis if needed to ensure it remains within the viewport. (Within reason, at a narrow enough viewport, the dropdown can overflow on both sides, at which point there’s not much more to do. But Decap does not contain any very wide dropdowns, so this scenario can be ignored I think.) For most viewports, there is no change and menus are displayed as before. But for small viewports this approach fixes things.

It is heavily inspired by Floating UI’s API and the hook added in this PR is roughly equivalent to:

import { useFloating, shift } from '@floating-ui/react-dom';

// Inside component
const { refs, floatingStyles } =  useFloating({ middleware: [shift] });

Decap’s needs are quite simple though, so I preferred to roll my own, minimal implementation, which is significantly smaller and less complex than Floating UI. I did a quick check to make sure the size benefits justified owning the code. The bundle size of this implementation is ~1.6 kB (753 B gzip) vs ~13.8 kB (5.52 kB gzip) for Floating UI. Add on the 500 kB install size of @floating-ui/react-dom and I think the benefits are pretty clear.

Test plan

Existing tests pass and I manually tested the positioning using the local dev environment. If you want, I guess it’s possible to add an E2E test that would use a small viewport and assert that clicking a dropdown shows its contents within the viewport, but I haven’t added that currently.

Checklist

Please add a x inside each checkbox:

A picture of a cute animal (not mandatory but encouraged)

@delucis delucis requested a review from a team as a code owner May 23, 2026 17:22
position: relative;
font-size: 14px;
user-select: none;
touch-action: manipulation;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not directly related to positioning, but improves support on touch devices by removing a delay between a user’s tap and the subsequent click event. More details on MDN

Comment on lines -47 to 50
left: ${props.position === 'left' ? 0 : 'auto'};
right: ${props.position === 'right' ? 0 : 'auto'};
left: ${props.left};
`};
`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new hook calculates a desired left co-ordinate based on requested left/right placement, so we don’t need to switch between left and right here anymore.

<StyledWrapper
closeOnSelection={closeOnSelection}
onSelection={handler => handler()}
onMenuToggle={({ isOpen }) => setOpen(isOpen)}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure if there’s a better way to access React Aria’s open state than this with the extra useState hook, but this is important to be able to skip most of the positioning logic except when a dropdown is actually open.

className={className}
>
{renderButton()}
<div ref={refs.source}>{renderButton()}</div>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory we could pass the ref directly to the dropdown buttons, but because the buttons are provided via a render prop, I preferred to wrap it for now. Happy to adjust this to be renderButton(ref) and update all the places that use <Dropdown> if that’s preferable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant