elastic / elastic/eui

[Feature Request] `EuiComboBox`: selection display modes (`collapse`, `count`) + optional selected-option pinning

Open
#9,939 1 comment 2 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
6.4k
Forks
911
Avg merge
2d 11h
Merged PRs (30d)
65

Description

# TL;DR video overview

https://drive.google.com/file/d/10x8wBYzGWikWSzPnBMkXEL6nHirgNjjC/view?usp=sharing

# Requestor

@MichaelMarcialis

# Problem statement

With multiple selections, `EuiComboBox` wraps selected option badges onto additional lines, growing the input vertically. The resulting height is unbounded and unpredictable, as it depends on both the number of selections and the length of their labels. Consuming layouts are unable to predict and reserve space for it. This makes the component difficult to use in horizontally constrained surfaces (ex. global headers, toolbars, table cells, inline filter rows, narrow flyouts, sidebars, and panels).

# Proposed solution

Two additive props. Existing usages would be unaffected.

## `selectionDisplay: 'wrap' | 'collapse' | 'count'` (default `'wrap'`)

| Mode | Input renders | Intended use |
| --- | --- | --- |
| `wrap` | All badges, wrapping to multiple lines | Forms and other surfaces with vertical room (unchanged) |
| `collapse` | Badges until horizontal overflow, then a `+N` counter | Constrained width where individual selections still matter |
| `count` | No badges; a summary label (ex. "3 projects selected") | Tightest surfaces, or high selection counts |

**Layout rule for `collapse`:** badges (left) → reserved text-cursor space → `+N` (right) → clear button → chevron.

**`+N` counter rule (both new modes):** `+N` counts selections **not currently visible in the field**.

| State | Field shows |
| --- | --- |
| `collapse`, resting, 1 of 3 badges fit | `corp-it-dev ×` … `+2` |
| `collapse`, typing | `dev` … `+3` |
| `count`, resting | `3 projects selected` |
| `count`, typing | `dev` … `+3` |

## `selectionPinning: boolean`

Selected options sort to the top of the option list. Order is computed **on popover open only**, never mid-session, so options never move under the user's cursor. Pinned selections still respect the active query. A selected option that doesn't match filters out like any other, so the pinned group never goes stale.

Defaults: `false` for `wrap` (backward compatibility), `true` for `collapse` and `count`.

Kept as a separate prop rather than folded into the display modes, because some consumers have lists whose order carries meaning (severity, recency, relevance ranking, grouped options) and pinning would corrupt it.

## Deliberate decisions worth calling out

**Badges collapse on first keystroke, not on focus.** Collapsing on focus removes each badge's `×` exactly when the user has focused the control. This would be a regression against `wrap` for keyboard users, who would then have no way to remove one specific selection without round-tripping through the list. It also breaks tooltip focus parity and makes `+N` change with no user action. Deferring to the first keystroke gives a single deliberate transition. Badges can then be restored on clear or blur.

**Text-cursor space is always reserved in the overflow calculation.** Badges should never pack up against the clear button. This guarantees room for the text-cursor on focus (so `+N` doesn't shift), and when unfocused the visible gap is what signals the field accepts typing.

**Search stays in the input.** No search field inside the popover, as that would change the trigger from `role="combobox"` to a button opening a dialog containing a searchable listbox (which is a different ARIA pattern varying by prop value). Both new modes instead reuse the text-replacement behavior `singleSelection={{ asPlainText: true }}` already implements.

**The `count` summary renders as value text, not placeholder gray.** There is a value; placeholder styling would imply nothing is selected.

# Use case

Any surface where a multi-select must sit in fixed horizontal space and cannot be allowed to change height:

- Global header or chrome controls, where vertical growth pushes or overlaps page content.
- Table cells and inline filter rows, where row height must stay uniform.
- Narrow flyouts, sidebars, and panels, where a wrapped badge row consumes most of the available height.
- Selections whose labels are long and similar (ex. resource names differing only by environment or region), where wrapping escalates quickly.

# Value/impact

- **Prevents divergence across Kibana.** Teams hitting this today build one-off multi-selects from `EuiSelectable` plus a custom trigger. Each is a slightly different interaction model, and none of them get the combobox pattern for free.
- **Keeps typeahead, custom values, and combobox ARIA semantics** in constrained surfaces, where they're currently traded away.
- **Makes layout predictable**, so consumers can reserve space for the control.
- **Accessibility handled once, centrally**, rather than re-derived per workaround (see additional context section).
- **Low adoption cost:** default `wrap` means no existing usage changes.

# Urgency

Not blocking. Workarounds exist, at the cost of divergence. There is near-term demand from at least one constrained surface (CPS project picker interface), and the number of hand-rolled replacements grows the longer this is unavailable.

# Do alternatives or workarounds exist?

Yes, all with meaningful costs:

| Workaround | Cost |
| --- | --- |
| `EuiSelectable` + custom popover trigger | Loses typeahead, `onCreateOption`, and the combobox ARIA pattern; reimplemented per consumer |
| Aggressively truncate option labels | Hides the discriminating part of similar names; doesn't stop wrapping, only delays it |
| Cap the number of selections | Product constraint imposed by a layout limitation |
| Fixed-height container with internal scroll | Selections hidden with no indication of how many; scroll region inside a form control |

Design directions considered and rejected during this proposal are in the "Additional context" section.

# Related code or customizations

- `selectedOptions` is already the controlled value array, so the display prop needs a distinct name. `selectionDisplay` proposed.
- `singleSelection={{ asPlainText: true }}` already implements the text-replacement behavior both new modes rely on. It is worth confirming the exact focus/blur semantics before implementation.
- Badge rendering and the existing overflow/wrap behavior in `EuiComboBox`.
- `truncationProps` — related to the option-label truncation note below.
- `EuiSelectable` — the component teams currently fall back to.

# Additional context

## Accessibility

- `+N` needs a tab stop and a descriptive label ("2 more selected"), not just the rendered glyph. Its tooltip must open on focus, not hover alone.
- **Backspace with no badges rendered** removes a selection with no visual feedback in `count`. Either suppress it when selections aren't individually rendered, or announce the removal via `aria-live`.
- Announce selection-count changes via `aria-live`.
- The `count` summary must be fully consumer-supplied and localized (message override or `(count) => string`), including singular/plural. Assistive tech always receives the complete sentence.
- When the container is too narrow for even one badge, `collapse` degrades to `count`.

## Considered and rejected

**An expanding badge row.** Expanding the input in flow reintroduces the layout instability the modes exist to prevent; rendering the expansion as a floating layer just duplicates the popover surface.

**A left-aligned count badge (`[3] projects selected`).** Survives typing by construction, but splitting the numeral out of the sentence breaks locales where the noun's form depends on the numeral (Slavic, Arabic) or where numeral position differs, and it removes the ability to pass a `(count) => string` override.

## Open questions

1. **Which badge survives collapse?** Proposal: the first in the list's sort order, so the visible badge doesn't change as the user selects.
2. **Resize:** the collapse threshold is width-dependent, so `N` changes on resize. Does the typing state persist across it?

# Designs or Specs

Figma flows for all three modes, each covering resting, hover/tooltip, focused with popover open, and typing with the list filtered: https://www.figma.com/design/FMyUmplTbpjdGm7tIKCw0J/EuiComboBox-enhancement-proposal?node-id=0-1&t=TFXroZoZLX3MW259-1

Image

CCing @jovana-andjelkovic, as I recall our discussing an enhancement like this in the past. Also CCing @eokoneyo, as this enhancement is something I'd like to implement in the CPS project picker, if it gets adopted.

Contributor guide

Open the contributing guide

Research direction

Start with the EuiComboBox implementation, its selectedOptions handling, badge rendering, and existing overflow/wrap behavior. Read the singleSelection={{ asPlainText: true }} behavior, then compare the proposal with EuiSelectable and truncationProps; the Figma flows define the display states. Done means the three display modes and optional pinning satisfy the stated focus, resize, keyboard, localization, and accessibility requirements.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, design, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.