Skip to content

[Bug]: Select and DatePicker do not show invalid styling when isInvalid is true #187

@rms-markand-bhatt

Description

@rms-markand-bhatt

Prerequisites

  • I have searched existing issues to ensure this bug hasn't been reported already
  • I have tested this with the latest version of the components

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

  1. Render a Select (or DatePicker) inside a form.
  2. Pass isInvalid={true}.
  3. 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

  • This bug affects keyboard navigation
  • This bug affects screen reader users
  • This bug affects focus management
  • This bug affects color contrast or visual accessibility

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions