Prerequisites
Bug description
Select and DatePicker accept an isInvalid prop, which is forwarded to the underlying react-aria-components root (setting data-invalid and aria-invalid for assistive tech). However, the visible trigger button rendered by each component hardcodes its ring colour and only branches styling on isOpen / isFocused / isDisabled. The invalid state never reaches the trigger visually, so the field looks identical whether it has an error or not.
Input is unaffected. Its wrapper does wire invalid state into the ring colour (see the bundled "has-[&>select]:ring-border-error_subtle" styling block).
This breaks form UX: zod validation errors and server-side field errors are set correctly via setError, the inline hint text appears in red, but the field outline stays neutral. A user can't tell at a glance which field is at fault.
Steps to reproduce
- Render a Select (or DatePicker) inside a form.
- Pass isInvalid={true}.
- Render the page in a browser.
Minimal repro:
import { Select, DatePicker } from ".";
function Repro() {
return (
<div className="flex flex-col gap-4">
<Select label="Area" isInvalid items={[{ id: "1", label: "One" }]} aria-label="Area">
{(item) => <Select.Item id={item.id}>{item.label}</Select.Item>}
</Select>
<DatePicker isInvalid aria-label="Arrive" />
</div>
);
}
Expected behavior
When isInvalid is true, the trigger button should render with the error ring token (ring-border-error_subtle), matching Input's behaviour. On focus while invalid, it should escalate to ring-border-error. This brings Select and DatePicker in line with the existing convention used inside Input.
Actual behavior
The trigger button keeps the default ring-1 ring-primary styling. The only visible signal of the error state is the inline hint text below the field (and the aria-invalid attribute, which is invisible to sighted users).
Code reference
const SelectTrigger = ({ isOpen, isFocused, isDisabled, size, placeholder, placeholderIcon, ref }) => (
<Group
ref={ref}
className={cx(
"relative flex w-full cursor-pointer items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
(isFocused || isOpen) && "ring-2 ring-brand",
isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled",
// no branch for invalid state
)}
...
isInvalid is not destructured here, and there is no (isInvalid) && "ring-..." branch. Same shape for the DatePicker trigger (ae button rendered with color: "secondary" and no invalid override).
For comparison, Input's wrapper does handle it correctly:
className: cx(
"has-[&>select]:shadow-xs has-[&>select]:ring-1 has-[&>select]:ring-border-primary ...",
isInvalid && "has-[&>select]:ring-border-error_subtle has-[&>select]:has-[input:focus]:ring-border-error"
),
Workaround
We applied a Tailwind descendant-selector override at every call site, scoped to the React-side error boolean. This conditionally targets the trigger button's ring colour without touching the library:
import { cn } from "@/utils/cx";
<Select
isInvalid={Boolean(error)}
className={cn(
error && "[&_button]:!ring-1 [&_button]:!ring-border-error_subtle",
)}
...
/>
Code example
Browser
No response
Device type
No response
Component version
No response
Environment details
No response
Screenshots/Videos
No response
Console errors
Additional context
No response
Accessibility impact
Prerequisites
Bug description
Select and DatePicker accept an isInvalid prop, which is forwarded to the underlying react-aria-components root (setting data-invalid and aria-invalid for assistive tech). However, the visible trigger button rendered by each component hardcodes its ring colour and only branches styling on isOpen / isFocused / isDisabled. The invalid state never reaches the trigger visually, so the field looks identical whether it has an error or not.
Input is unaffected. Its wrapper does wire invalid state into the ring colour (see the bundled "has-[&>select]:ring-border-error_subtle" styling block).
This breaks form UX: zod validation errors and server-side field errors are set correctly via setError, the inline hint text appears in red, but the field outline stays neutral. A user can't tell at a glance which field is at fault.
Steps to reproduce
Minimal repro:
Expected behavior
When isInvalid is true, the trigger button should render with the error ring token (ring-border-error_subtle), matching Input's behaviour. On focus while invalid, it should escalate to ring-border-error. This brings Select and DatePicker in line with the existing convention used inside Input.
Actual behavior
The trigger button keeps the default ring-1 ring-primary styling. The only visible signal of the error state is the inline hint text below the field (and the aria-invalid attribute, which is invisible to sighted users).
Code reference
isInvalid is not destructured here, and there is no (isInvalid) && "ring-..." branch. Same shape for the DatePicker trigger (ae button rendered with color: "secondary" and no invalid override).
For comparison, Input's wrapper does handle it correctly:
Workaround
We applied a Tailwind descendant-selector override at every call site, scoped to the React-side error boolean. This conditionally targets the trigger button's ring colour without touching the library:
Code example
Browser
No response
Device type
No response
Component version
No response
Environment details
No response
Screenshots/Videos
No response
Console errors
Additional context
No response
Accessibility impact