---
title: Select
description: A dropdown select component with validation states, icons, and FormControl integration for form inputs
---

```tsx
'use client';

import { Select } from '@/ui/forms/Select';

export function SelectDemo() {
  return (
    <Select name="country" label="Country" description="Select your country">
      <option value="">Choose a country</option>
      <option value="us">United States</option>
      <option value="uk">United Kingdom</option>
      <option value="ca">Canada</option>
      <option value="fr">France</option>
    </Select>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/select.json
```

Or manually, copy the following code into `/ui/forms/select.tsx`:

```tsx
import clsx from 'clsx';
import { useValue } from '../hooks/useValue';
import { useInternalStatus } from '../hooks/useInternalStatus';
import { FormControl, type FormControlProps } from './FormControl';
import { Icon, type IconicTypes } from '@clubmed/trident-icons';
import { type SelectHTMLAttributes, useId } from 'react';

export interface SelectProps<Value>
  extends FormControlProps<Value, SelectHTMLAttributes<HTMLSelectElement>> {
  description?: string;
  iconType?: IconicTypes;
  errorMessage?: string;
  dataTestId?: string;
}

export const Select = <Value = string,>(props: SelectProps<Value>) => {
  const internalId = useId();

  const {
    id = internalId,
    name = id,
    label,
    value: initialValue,
    description,
    validationStatus = 'default',
    iconType,
    errorMessage,
    disabled = false,
    required = false,
    hideRequiredStar,
    className,
    dataTestId = id,
    onChange,
    children,
    ...rest
  } = props;

  const { value, setValue } = useValue<Value>({ initialValue, onChange });
  const internalStatus = useInternalStatus({
    isDisabled: disabled,
    validationStatus,
  });

  return (
    <FormControl
      id={id}
      className={className}
      label={label}
      description={description}
      dataName="TextField"
      dataTestId={dataTestId}
      disabled={disabled}
      required={required}
      hideRequiredStar={hideRequiredStar}
      validationStatus={validationStatus}
      errorMessage={errorMessage}
    >
      <div
        className={clsx('relative rounded-pill z-0', {
          'bg-white': internalStatus !== 'disabled',
          'bg-pearl inset-ring-middleGrey': internalStatus === 'disabled',
        })}
      >
        <select
          {...rest}
          id={id}
          name={name}
          disabled={disabled}
          required={required}
          value={value as any}
          onChange={(e) => {
            setValue(e.target.value as Value, e.nativeEvent);
          }}
          className={clsx(
            'text-b3 rounded-pill w-full inset-ring overflow-hidden px-20 py-12 font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 appearance-none bg-transparent',
            {
              'inset-ring-middleGrey focus:inset-ring-black active:inset-ring-black pe-[52px]':
                internalStatus === 'default',
              'pe-[84px]': internalStatus === 'error' || internalStatus === 'success',
              'text-black': internalStatus !== 'disabled',
              'bg-pearl inset-ring-middleGrey': internalStatus === 'disabled',
              'inset-ring-red': internalStatus === 'error',
              'inset-ring-green': internalStatus === 'success',
            },
          )}
          aria-label={name}
        >
          {children}
        </select>

        <div
          className={clsx(
            'pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12 -z-1',
            {
              'text-grey': internalStatus === 'disabled',
              'text-red': internalStatus === 'error',
              'text-green': internalStatus === 'success',
            },
          )}
        >
          <span className="ms-auto flex gap-x-8">
            {internalStatus === 'error' && (
              <Icon name="CrossDefault" width="24px" type={iconType} />
            )}

            {internalStatus === 'success' && (
              <Icon name="CheckDefault" width="24px" type={iconType} />
            )}

            <span className="pointer-events-auto inline-flex">
              <Icon name="ArrowDefaultDown" type="svg" width="24px" color="black" />
            </span>
          </span>
        </div>
      </div>
    </FormControl>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { Select } from '@/docs/lib/ui/forms/select';
import { useState } from 'react';

export function Component() {
  const [country, setCountry] = useState('');

  return (
    <Select
      label="Country"
      description="Select your country"
      value={country}
      onChange={(_, value) => setCountry(value)}
      required
    >
      <option value="">Choose a country</option>
      <option value="us">United States</option>
      <option value="uk">United Kingdom</option>
      <option value="ca">Canada</option>
      <option value="fr">France</option>
    </Select>
  );
}
```

## API Reference

### Select

| Prop               | Type                                      | Default        | Description                                                               |
| ------------------ | ----------------------------------------- | -------------- | ------------------------------------------------------------------------- |
| `label`            | `string`                                  | `undefined`    | Label text displayed above the select input                               |
| `description`      | `string`                                  | `undefined`    | Helper text displayed below the label                                     |
| `id`               | `string`                                  | Auto-generated | Unique identifier for the select element                                  |
| `name`             | `string`                                  | Same as `id`   | Name attribute for form submission                                        |
| `value`            | `Value` (generic)                         | `undefined`    | Current selected value (controlled component)                             |
| `onChange`         | `(event: Event, value: Value) => void` | `undefined` | Callback fired when selection changes, receives event and new value |
| `disabled`         | `boolean`                                 | `false`        | Whether the select is disabled                                            |
| `required`         | `boolean`                                 | `false`        | Whether the select is required (shows asterisk in label)                  |
| `hideRequiredStar` | `boolean`                                 | `false`        | Hides the required asterisk when true                                     |
| `validationStatus` | `"default" \| "error" \| "success"`       | `"default"`    | Visual validation state with colored borders and icons                    |
| `errorMessage`     | `string`                                  | `undefined`    | Error message displayed below the select when validationStatus is "error" |
| `iconType`         | `IconicTypes`                             | `undefined`    | Icon type for validation icons                                            |
| `dataTestId`       | `string`                                  | Same as `id`   | Test ID for testing purposes                                              |
| `className`        | `string`                                  | `undefined`    | Additional CSS classes for the FormControl container                      |
| `children`         | `React.ReactNode`                         | `undefined`    | Option elements (`<option>`) to display in the dropdown                   |
| ...rest            | `SelectHTMLAttributes<HTMLSelectElement>` | -              | All standard HTML select attributes are supported                         |

## Notes

- **Generic Value Type**: The Select component supports generic value types via `Select<Value>`, allowing type-safe value handling for different data types (strings, numbers, objects, etc.).

- **Automatic State Management**: Uses the `useValue` hook internally. When `onChange` is provided, it is called with `(event, value)`.

- **FormControl Integration**: The select is wrapped in a `FormControl` component that handles label, description, required indicator, and error message display consistently with other form components.

- **Validation States**: Three visual states available:

  - `default`: Light grey border with focus/active states
  - `error`: Red border with cross icon
  - `success`: Green border with checkmark icon

- **Validation Icons**: Error and success states automatically display appropriate icons (cross for error, checkmark for success) positioned to the left of the dropdown arrow.

- **Dropdown Arrow**: A fixed dropdown arrow icon is always displayed on the right side of the select, styled with a custom button appearance.

- **Disabled State**: When disabled, the select has a pearl background, grey text, and cannot be interacted with.

- **Rounded Pill Design**: The select uses rounded pill styling (`rounded-pill` class) for a modern, friendly appearance.

- **Automatic ID Generation**: If no `id` is provided, one is automatically generated using React's `useId` hook. The `name` defaults to the `id` if not specified.

- **Accessibility**: The select has proper ARIA attributes including `aria-label` set to the name attribute, and standard select accessibility with combobox role.

- **Dynamic Padding**: The select adjusts its right padding based on validation state to accommodate validation icons (52px for default, 84px for error/success).

- **Data Attributes**: The component includes `data-name="Select"` and `data-testid` attributes for testing and debugging purposes.

- **Client Component**: This is a client component ("use client") due to its use of React hooks (useState, useId) and interactive functionality.
