Skip to content

Conversation

@RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Sep 25, 2025

This PR fixes an issue with the Listbox component where we didn't freeze the value soon enough.

This happens when state lives in the parent, and is updated via an onChange.

What is currently happening:

  1. User clicks on a listbox option, this should do 3 things:
  2. Call the onChange with the new value
  3. Close the listbox
  4. "Freeze" the value, so the old value is still showing while the listbox options are closing.

The problem is that calling the onChange updates the value in the parent, and the component re-renders with the new value. At the time we freeze the value, we already received the new value so we are freezing the incorrect value. This causes a visual glitch. See reproduction: tailwindlabs/tailwind-plus-issues#1761

This PR fixes that by changing the order a little bit so we freeze the value as early as possible.

So now, when the user clicks on an option, we trigger a SelectOption action. This will track whether we should freeze the value or not in state immediately. After that, we call the onChange, and then close the listbox.

Since we know we want to freeze the value before calling onChange, we can be sure we are freezing the correct (old) value.

Test plan

Made a little video but with a duration of 1000 instead of 100 so you can clearly see the old value and no visual jumps while the listbox is closing.

Screen.Recording.2025-09-25.at.14.33.49.mov

Fixes: tailwindlabs/tailwind-plus-issues#1761

We are only interested in CSS Transitions not Animations in this case.
We are also interested in the ones that are not `finished` yet.

We implemented this in Elements as well.
The moment we select an option in single value mode we will freeze the
state immediately. This means that we don't have to rely on additional
re-renders to freeze the value which could be too late.

It's too late when between the `onChange` and `closeListbox` a re-render
happens with the new value. Because then we would freeze the new value
which would be wrong.

This also slightly refactors the `selectActiveOption` and
`selectOption` actions.
@vercel
Copy link

vercel bot commented Sep 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
headlessui-react Ready Ready Preview Comment Sep 25, 2025 1:04pm
headlessui-vue Ready Ready Preview Comment Sep 25, 2025 1:04pm

done() {
if (cancelledRef.current) {
if (typeof element.getAnimations === 'function' && element.getAnimations().length > 0) {
if (hasPendingTransitions(element)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Ported this from Elements because during my debugging I noticed that sometimes the transitions themselves were glitching when the component re-rendered with the new value while a transition was in progress.

We don't have to know whether we are transitioning or not. Because we
will always freeze the value in single value mode.
Didn't do that initially because this exact block exists somewhere else,
but the `else if` is a little bit cleaner syntax wise.
@RobinMalfait RobinMalfait merged commit da2fa94 into main Sep 25, 2025
8 checks passed
@RobinMalfait RobinMalfait deleted the fix/tailwind-plus-issues-1761 branch September 25, 2025 13:11
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.

Listbox flickering/snapping around when new selection is made

3 participants