mui / mui/base-ui

[select] Disabled trigger's value can't be selected or copied with the mouse

Open
#5,736 1 comment 0 reactions 0 assignees View on GitHub
status: waiting for maintainer
Dominant language
TypeScript
Stars
10.9k
Forks
543
Avg merge
1d 20h
Merged PRs (30d)
101

Description

# Bug report

## Current behavior

A `Select.Root` with `disabled` renders the chosen value into the trigger, and that text can't be selected with the mouse: a double-click selects nothing, and dragging across it from inside selects nothing. The same select without `disabled` selects it normally.

The text is selectable in principle — a selection dragged from *outside* passes straight through and includes it (`"…bel\nOption two disabled"` in my run). Only a press that starts inside the trigger is refused.

It isn't CSS. `user-select` and `pointer-events` compute to `auto` on the trigger and on the value element in all three states (default, `readOnly`, `disabled`).

The cause is one line in `useButton`, which `SelectTrigger` routes through ([`SelectTrigger.tsx#L90-L93`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/trigger/SelectTrigger.tsx#L90-L93)):

[`useButton.ts#L218-L224`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/internals/use-button/useButton.ts#L218-L224)

```ts
onPointerDown(event: React.PointerEvent) {
if (disabled) {
event.preventDefault();
return;
}
externalOnPointerDown?.(event);
},
```

`preventDefault()` on `pointerdown` cancels the text selection the browser would otherwise start, along with the focus it is presumably there to suppress. The neighbouring `onMouseDown` handler shows the alternative shape — it returns without preventing anything ([`#L111-L115`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/internals/use-button/useButton.ts#L111-L115)).

## Expected behavior

A disabled control is not operable, but its text is still content: the value is on screen precisely so it can be read, and reading includes selecting and copying it. So `disabled` should leave the trigger's text selectable.

**What the platform does, measured in all three engines** (Playwright, one drag across each element, `window.getSelection()`):

| element | Chromium | Firefox | WebKit |
| --- | --- | --- | --- |
| `Hello world value` | no | no | no |
| `Hello world value` | no | no | no |
| `

` | selects | selects | selects |
| `
` | selects | selects | selects |
| the same div plus one `pointerdown` `preventDefault()` | no | no | no |
| `

` | selects | selects | selects |

A real `` never allows it, in either state. A div carrying the combobox role always allows it, `aria-disabled` included. `Select.Trigger` matches neither: it allows it while enabled and refuses while disabled, and the table's last row is exactly what makes the difference.

That matters because the trigger is deliberately **not** a button: it is `role="combobox"` ([`#L132`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/trigger/SelectTrigger.tsx#L132)), and the file even defends that role against a nested `useButton` ([`#L211-L213`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/trigger/SelectTrigger.tsx#L211-L213)). A combobox trigger displays the selected value, which is content; `useButton` is an implementation detail it borrows for activation, and its disabled branch takes away a capability the role never implied.

Two ways out, either works for me:

1. **Narrow the suppression in `useButton`** so that it stops the disabled control taking focus on press without stopping the selection. (Focus suppression looks like the point of the line: the element carries `tabIndex: -1` while disabled, [`#L141`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/trigger/SelectTrigger.tsx#L141), and that is focusable by pointer.)
2. **Don't route `Select.Trigger`'s `disabled` through `useButton`'s pointer handling at all.** The select's interaction is already gated at the root — [`useClick` `enabled: !disabled`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/root/SelectRoot.tsx#L348-L351), [`useListNavigation`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/root/SelectRoot.tsx#L355-L356), [`useTypeahead`](https://github.com/mui/base-ui/blob/ca182273cc0013bacd443e4ab03d4bd4f8cc1f35/packages/react/src/select/root/SelectRoot.tsx#L372-L375) — so what `useButton({ disabled })` contributes here is mainly the focusable-when-disabled tabIndex and ARIA, not the pointer suppression.

To preempt the third option: making it consistent the *other* way, so a disabled and an enabled trigger both refuse selection like a native button, would also be self-consistent — but it would mean no `Select` value can ever be copied while disabled, which is the thing this report is about.

## Reproducible example

```jsx
import { Select } from '@base-ui/react/select';

export default function App() {
return (







Option a
Option b




);
}
```

1. Double-click the value in the trigger, or press and drag across it — nothing is selected.
2. Start the drag just above the trigger and release just below it — the value is included in the selection, so the text itself is selectable.
3. Remove `disabled` and repeat step 1 — the value is selected (the popup opens too, since the press also opens it).

The mechanism on its own, with no Base UI involved — the second box refuses the selection in all three engines, and the only difference between them is that one listener:

```html

Hello world value

Hello world value

document.getElementById('prevented')
.addEventListener('pointerdown', (e) => e.preventDefault());

```

## Base UI version

v1.8.0. Unchanged on `master` as of ca182273cc0013bacd443e4ab03d4bd4f8cc1f35.

## Which browser are you using?

Chromium, Firefox and WebKit, all three through Playwright, with identical results in each — both for the Base UI case and for the minimal repro above.

## Which OS are you using?

Linux (WSL2). Not OS-specific.

## Which assistive tech are you using (if applicable)?

None — this is a pointer-interaction report, not a screen-reader observation.

## Additional context

**Where this came from.** The same read-only/disabled configuration form as #5530: values stay meaningful and on screen when the revision is locked, and people copy them out. `readOnly` is unaffected — the value selects there — so this is specifically the `disabled` path.

**Scope I verified.** I measured `Select`. Anything else that hands a non-native element to `useButton({ disabled })` and renders text worth copying inherits the same behaviour, but I haven't enumerated those, so I've kept the title on `Select` rather than claiming a set I didn't check.

**No local workaround shipped.** Both of the ones available to us are structural — not rendering `Select.Trigger` while disabled, or moving the value outside it — and both cost more than the defect, so we're leaving it as is and would rather see it fixed here. Happy to send the PR for whichever of the two directions you prefer.

Contributor guide

Open the contributing guide

Research direction

Start in packages/react/src/internals/use-button/useButton.ts, comparing the disabled pointer handler with the neighboring mouse handler, then trace how packages/react/src/select/trigger/SelectTrigger.tsx routes through useButton. Check the disabled Select repro across the described browser cases and the SelectRoot interaction gates. Done means the disabled trigger value can be selected and copied without restoring disabled activation or focus behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.