Skip to content

feat(mosaic): add Combobox.Clear and consistent trigger interactions - #10689

Open
prestonwebdev wants to merge 4 commits into
prestonwebdev/figma-menu-spacing-updatefrom
prestonwebdev/mosaic-trigger-interactions
Open

prestonwebdev wants to merge 4 commits into
prestonwebdev/figma-menu-spacing-updatefrom
prestonwebdev/mosaic-trigger-interactions

Conversation

@prestonwebdev

@prestonwebdev prestonwebdev commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Description

Stacked on #10688 (which is stacked on #10690): this diff shows only the interaction changes, so merge those first.

Change Why
New Combobox.Clear part. It renders only while the input has text or a selection, and clicking it empties both, keeps focus on the input and calls onValueChange(null). It has its own aria-label, Clear by default A one-click way to empty the field. It is a separate part, so the trigger never has to switch between toggling and clearing.
Combobox.Trigger stays a plain toggle. It keeps its own label and its aria-expanded and aria-controls while there is text. With no render the styled trigger renders a Mosaic Button with a chevron, and children only change its content The list can still be opened and closed while typing, and the rotated chevron stays visible. Note this is a change to a public part: the styled trigger used to render a plain <button> when render was not passed.
The trailing X and chevron are dimmed until the field is hovered (new InputGroup hover marker), and sit 8px apart The hover target is the whole field, not just the icon.
The combobox and phone input chevrons rotate 180° while open (0.15s, instant under prefers-reduced-motion). The Select does not rotate It signals the open state. The Select popup opens over its trigger, so a rotation wouldn't be visible.
returnFocus={false} on the popup focus manager Clicking outside now closes the list and blurs the input instead of putting focus back and leaving the ring on.
Opening on input click uses Floating UI's useClick (openOnInputClick, on by default in the styled Combobox). Clicking an InputGroup's padding opens the list, handled by the styled Combobox It reuses the click handling that popover, menu and select already use. InputGroup itself is not changed, so its other consumers are unaffected.
Select trigger hover darkens the border and leaves the fill alone. Pressed and open still fill Matches an Input's hover. A fill is reserved for state.
The default Select trigger is as wide as its widest option, and the default outline trigger is at least 132px The trigger no longer changes width when the selection changes. Hidden copies of each label are drawn with CSS, so they add no text to the DOM or the accessibility tree. This applies to every trigger that uses the default value, including the ghost variant and custom render triggers like pagination's page size, which grow slightly to fit their longest option. Multiple selects and triggers with custom children are unchanged. The 132px minimum applies to the default outline trigger only.
Phone input trigger: the whole left box is the click target, and its chevron dims and darkens on hover A bigger target, and the same behavior as the combobox.
The combobox stories show Combobox.Clear beside Combobox.Trigger, and the docs table lists the new part Shows the behavior and documents the new part.
Tests for clearing and refocusing, Clear only rendering while there is a value, the trigger toggling with its own name while there is text, opening from the group padding, and the Select width sizers Cover the new behavior.

Components to check:

  • Combobox (Swingset Combobox, Default and Scrolling): type and select, click the X to clear, toggle the list from the chevron while there is text, watch the chevron rotate while open, click outside to blur, click the input and the field padding to open.
  • Select (Swingset Select): hover darkens the border with no fill, pressed and open still fill, and the trigger keeps its width as you change the selection. Also check the ghost variant in the members table and the pagination page size, which now size to their longest option.
  • PhoneInput (Swingset PhoneInput, the add phone dialog, the phone steps): click anywhere in the flag box, the chevron rotates, clicking outside the country list closes it without a stray focus ring.
  • InputGroup (Swingset InputGroup): other inputs in groups, such as the sign-in password step and the API key dialog, should behave exactly as before.
  • App consumers: the combobox primitive is only used by the phone input country list. Any Select in the app gets the new hover.

To review later: the field hover border color light-dark(#bebebe, #525252) is now defined locally in four components: input, radio and checkbox already on main, plus select here. This follows the existing pattern rather than adding a token. Whether a shared token (for example --cl-color-border-strong) is worth it is a separate question, given the StyleX guide prefers color-mix() at the call site over new tokens.

The changeset is empty, like the other recent Mosaic PRs.

Before and after screenshots: link to be added

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

🤖 Generated with Claude Code

@vercel

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Nov 6, 2026 3:17am UTC
swingset Ready Ready Preview Nov 6, 2026 3:17am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d8b633b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@blacksmith-sh

blacksmith-sh Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Matches Clearable combobox trigger clears field

The X shows only while there is text or a selection. Clicking it empties both, keeps focus in the input, fires onValueChange(null) and disappears. Without a label it is named Clear.

Recording

Watch full video · Original footage · 0:00 Type into the combobox · 0:09 Select an option · 0:14 Clear with the X · 0:21 Reopen the list

Clear fires onValueChange(null)

Also checked · 8 match

Matches Click outside closes and blurs combobox
Matches Clicking the field opens the list
Matches Select hover darkens border, no fill
Matches Phone input flag box click target
Matches Trigger keeps toggling while there is text
Matches Select trigger width stays fixed
Matches Ghost and pagination Selects unchanged
Matches Other InputGroups unchanged

Proof on d8b633bf (from b10ebf22) · Did Proof get it right? Yes · No

View full Proof report

@alexcarpenter alexcarpenter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two structural concerns (a trigger that switches between toggle and clear, and combobox logic in InputGroup), plus a few smaller notes inline.

const state = { open };
const { render, clearable = false, clearLabel = 'Clear', ...otherProps } = props;
const { open, setOpen, focusInput, popupId, triggerRef, hasValue, clear } = useComboboxContext();
const clearing = clearable && hasValue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This makes one button mean two things, and the clearing flag then changes the ARIA, the label, the click handler and the icon.

  • Typing opens the list (handleInputChange → setOpenState(true) in combobox-root.tsx) and also switches this into clearing mode. So the trigger drops aria-expanded/aria-controls exactly when the popup is open.
  • While there is text, the trigger can't toggle the list, and the rotated chevron is never visible.
  • A pointer resting on the chevron becomes a Clear button as soon as the user types.

Base UI (1.5.0 is in our node_modules) ships Combobox.Clear and Combobox.Trigger as separate parts. A Combobox.Clear that calls clear() and renders only while there is a value would delete clearing, the conditional ARIA spread, the aria-label override, clearLabel, and the stacked-icon opacity swap. The trigger would stay as it was. Any reason not to go that way?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good callout, fixed this one.

clearing: (value: boolean): Record<string, string> | null => (value ? { 'data-clearing': '' } : null),
},
props: mergeProps<'button'>(defaultProps, otherProps),
props: mergeProps<'button'>(defaultProps, clearing ? { ...otherProps, 'aria-label': clearLabel } : otherProps),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This silently replaces a consumer's own aria-label while clearing. A separate Clear part with its own label avoids the override entirely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! Also fixed

return;
}
inputElementRef.current?.focus();
inputElementRef.current?.click();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This puts combobox behavior into InputGroup: every InputGroup input now gets a synthetic click() on a padding click. There are 8 consumers outside the component itself, including the sign-in password step and the API key dialog. Could the styled combobox own this instead? It already knows the group element, since it uses it as the anchor.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should be fixed as well

onChange(event: React.ChangeEvent<HTMLInputElement>) {
handleInputChange(event.target.value);
},
onClick() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This rebuilds Floating UI's useClick, which popover, menu, select and drawer already use (popover-root.tsx:103, menu-root.tsx:129). Adding useClick(floatingContext, { enabled: openOnInputClick && !inline, toggle: false }) to the root's useInteractions would remove this handler and the openOnInputClick context field. getReferenceProps is already spread onto the input.

focusInput();
}, [focusInput, setInputValue, setSelectedValue]);

const hasValue = inputValue !== '' || selectedValue !== null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

inputValue and selectedValue are already on the context, so hasValue duplicates them. With a separate Clear part, this field isn't needed.

handleSelect,
handleInputChange,
setOpen,
openOnInputClick: Boolean(props.openOnInputClick) && !props.inline,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See the useClick note on combobox-input.tsx. With that, this context field and its two memo deps go away.

) {
const defaultRender: ComboboxTriggerProps['render'] =
render ??
(children === undefined

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Whether children were passed now decides which element renders. With clearable plus custom children, nothing renders the X, but the data-clearing behavior still applies. This goes away if clearing is its own part.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Made clearing independent. I got the idea originally from the geist design system, but what I updated it with follows more closely to what Kumo does. I think it works just as well, but feels a bit better than what we had originally imo.

expect(screen.getByRole('button', { name: 'Toggle fruit options' })).toBeInTheDocument();
});

it('keeps toggling the list when the trigger is not clearable', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The name says it keeps toggling the list, but the test only checks that there's no Clear button. Either assert that the trigger opens and closes the list, or rename the test.

… interactions

Adds an opt-in `clearable` Combobox.Trigger that swaps the chevron for a clear
button, dims trailing chevrons until hover, rotates the combobox and phone
input chevrons while open, gives the Select trigger an input-style hover
border, and makes the phone input's whole country box the click target.
Clicking outside a combobox now blurs the input.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
prestonwebdev and others added 3 commits November 5, 2026 20:14
…ories

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a 132px minimum

Hidden copies of each label share the value slot, so the trigger no longer
changes width when the selection does. The outline trigger is at least 132px.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds a Combobox.Clear part that renders only while there is text or a selection,
so the trigger stays a plain toggle with its own label and ARIA state. Opening on
input click now uses Floating UI's useClick, and the styled combobox, not
InputGroup, opens the list when the group padding is clicked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – swingset — d8b633bf Deployed Nov 6, 2026 by vercel[bot]
Preview – clerk-js-sandbox — d8b633bf Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants