Bug description
<Select.Value placeholder="..." /> renders an empty <span> when the items prop on <Select.Root> is a Record<string, ReactNode> that doesn't contain a "null" key. The placeholder text is completely absent from the DOM.
Steps to reproduce
const items = {
sans: 'Sans-serif',
serif: 'Serif',
mono: 'Monospace',
};
<Select.Root items={items}>
<Select.Trigger>
<Select.Value placeholder="Choose a font..." />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List>
{Object.entries(items).map(([value, label]) => (
<Select.Item key={value} value={value}>
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
))}
</Select.List>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
Expected: The trigger shows "Choose a font..." as placeholder text.
Actual: The trigger renders an empty <span>.
Root cause
The logic in hasNullItemLabel() in packages/react/src/utils/resolveValueLabel.tsx is inverted for the Record branch:
export function hasNullItemLabel(items: ItemsInput): boolean {
if (!Array.isArray(items)) {
return items != null && !('null' in items); // ← bug: `!` inverts the check
}
// ...
}
For a Record like { sans: "Sans-serif", serif: "Serif" }:
'null' in items → false (no "null" key)
!('null' in items) → true
- Function returns
true, meaning "the null value has a label"
This causes SelectValue to skip the placeholder (line 52):
} else if (!hasSelectedValue && placeholder != null && !hasNullLabel) {
children = placeholder;
}
Since hasNullLabel is true, the condition is false, and the placeholder is never rendered.
The fix should be removing the !:
return items != null && ('null' in items);
The existing tests in resolveValueLabel.test.ts only cover grouped array items — no test covers the Record branch.
Workaround
Use array format for items instead of Record:
const items = [
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
];
The array path in hasNullItemLabel has correct logic (item.value == null check).
Environment
Bug description
<Select.Value placeholder="..." />renders an empty<span>when theitemsprop on<Select.Root>is aRecord<string, ReactNode>that doesn't contain a"null"key. The placeholder text is completely absent from the DOM.Steps to reproduce
Expected: The trigger shows "Choose a font..." as placeholder text.
Actual: The trigger renders an empty
<span>.Root cause
The logic in
hasNullItemLabel()inpackages/react/src/utils/resolveValueLabel.tsxis inverted for the Record branch:For a Record like
{ sans: "Sans-serif", serif: "Serif" }:'null' in items→false(no"null"key)!('null' in items)→truetrue, meaning "the null value has a label"This causes
SelectValueto skip the placeholder (line 52):Since
hasNullLabelistrue, the condition isfalse, and the placeholder is never rendered.The fix should be removing the
!:The existing tests in
resolveValueLabel.test.tsonly cover grouped array items — no test covers the Record branch.Workaround
Use array format for
itemsinstead of Record:The array path in
hasNullItemLabelhas correct logic (item.value == nullcheck).Environment
@base-ui/reactversion: 1.2.0 (also present in 1.1.0)placeholderprop toValuepart #3604