[select] Disabled trigger's value can't be selected or copied with the mouse
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):
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).
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 |
|---|---|---|---|
<button>Hello world value</button> |
no | no | no |
<button disabled>Hello world value</button> |
no | no | no |
<div role="combobox" tabindex="0"> |
selects | selects | selects |
<div role="combobox" tabindex="-1" aria-disabled="true"> |
selects | selects | selects |
the same div plus one pointerdown preventDefault() |
no | no | no |
<p> |
selects | selects | selects |
A real <button> 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), and the file even defends that role against a nested useButton (#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:
- Narrow the suppression in
useButtonso 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 carriestabIndex: -1while disabled,#L141, and that is focusable by pointer.) - Don't route
Select.Trigger'sdisabledthroughuseButton's pointer handling at all. The select's interaction is already gated at the root —useClickenabled: !disabled,useListNavigation,useTypeahead— so whatuseButton({ 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
import { Select } from '@base-ui/react/select';
export default function App() {
return (
<Select.Root defaultValue="b" disabled>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.Item value="a">Option a</Select.Item>
<Select.Item value="b">Option b</Select.Item>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
}- Double-click the value in the trigger, or press and drag across it — nothing is selected.
- 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.
- Remove
disabledand 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:
<div id="plain" role="combobox" tabindex="0">Hello world value</div>
<div id="prevented" role="combobox" tabindex="0">Hello world value</div>
<script>
document.getElementById('prevented')
.addEventListener('pointerdown', (e) => e.preventDefault());
</script>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.
Source: mui/base-ui