Select

Selects let users select one or more items from a list.

Usage

Instalation

Run the following command:

npm i @radix-ui/react-select @radix-ui/react-scroll-area @radix-ui/react-label

Copy and paste the following code into your project.

'use client'

import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import * as SelectPrimitive from '@radix-ui/react-select'

import { cn } from '@/lib/utils'
import { Icon } from '@/components/ui/icon'

const outlinedLabelBeforeAndAfter = [
  /** Before **/
  'before:pointer-events-none before:mr-1 before:mt-[6px] before:box-border before:block before:h-full before:w-3.5 before:rounded-l-sm before:border-outline before:duration-200 before:transition-all before:border-t before:border-l',

  'group-data-[placeholder]/input:before:border-transparent',

  'group-data-[state=open]/input:before:border-t-2 group-data-[state=open]/input:before:border-l-2 group-data-[state=open]/input:before:border-primary',

  'group-focus/input:before:border-t-2 group-focus/input:before:border-l-2 group-focus/input:before:!border-primary',

  'group-aria-[invalid=true]/input:before:!border-error group-aria-[invalid=true]/input:before:border-l-2 group-aria-[invalid=true]/input:before:border-t-2',

  'group-disabled/input:before:border-t-onSurface/12 group-disabled/input:before:border-l-onSurface/12 group-disabled/input:group-data-[placeholder]/input:before:border-transparent',

  /** After **/
  'after:h-full after:pointer-events-none after:ml-1 after:mt-[6px] after:box-border after:block after:flex-grow after:rounded-r-sm after:border-outline after:duration-200 after:transition-all after:border-r after:border-t',

  'group-data-[placeholder]/input:after:border-transparent',

  'group-data-[state=open]/input:after:border-t-2 group-data-[state=open]/input:after:border-r-2 group-data-[state=open]/input:after:border-primary',

  'group-focus/input:after:border-t-2 group-focus/input:after:border-r-2 group-focus/input:after:!border-primary',

  'group-aria-[invalid=true]/input:after:!border-error group-aria-[invalid=true]/input:after:border-r-2 group-aria-[invalid=true]/input:after:border-t-2',

  'group-disabled/input:after:border-t-onSurface/12 group-disabled/input:after:border-r-onSurface/12 group-disabled/input:group-data-[placeholder]/input:after:border-transparent',
]

const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value

const FilledSelectRoot = (
  props: React.ComponentProps<typeof SelectPrimitive.Root>
) => {
  return <SelectPrimitive.Root {...props} />
}

const OutlinedSelectRoot = (
  props: React.ComponentProps<typeof SelectPrimitive.Root>
) => {
  return <SelectPrimitive.Root {...props} />
}

const FilledSelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
    error?: boolean
  }
>(({ className, children, error, ...props }, ref) => {
  return (
    <SelectPrimitive.Trigger
      ref={ref}
      aria-invalid={error ? 'true' : undefined}
      className={cn(
        'group/input peer/input relative z-0 h-14 w-full grow rounded-sm bg-surfaceContainer px-4 py-2 text-start text-body-lg text-onSurface outline-none transition-colors focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 data-[placeholder]:text-onSurfaceVariant/50',

        'disabled:!border-onSurface/12 disabled:bg-surfaceContainer/38 data-[label]:pt-5',

        'before:border-b-1 before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:border-onSurfaceVariant/12 before:transition-[color,border] hover:before:border-b-[2px] focus:before:border-b-[2px] focus:before:border-primary aria-[invalid=true]:before:border-b-[2px] aria-[invalid=true]:before:!border-error data-[state=open]:before:border-b-[2px] data-[state=open]:before:border-primary',

        'has-[label]:pt-3 has-[label]:data-[placeholder]:data-[state=open]:text-onSurfaceVariant/50 has-[label]:data-[placeholder]:text-transparent has-[label]:data-[placeholder]:focus:text-onSurfaceVariant/50',
        className
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon
        asChild
        className="absolute right-2 top-2/4 grid -translate-y-2/4 place-items-center text-onSurfaceVariant/70 transition-transform group-disabled/input:text-onSurface/38 group-data-[state=open]/input:rotate-180 [&>i]:text-[20px] [&>svg]:h-5 [&>svg]:w-5"
      >
        <Icon symbol="expand_more" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
})
FilledSelectTrigger.displayName = 'FilledSelectTrigger'

const FilledSelectLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(
      'pointer-events-none absolute left-0 top-0 z-10 flex h-fit  w-full select-none pl-4 pt-1 text-label-sm font-normal text-onSurfaceVariant duration-200',

      'group-data-[placeholder]/input:pt-4 group-data-[placeholder]/input:text-body-lg group-data-[placeholder]/input:text-onSurfaceVariant/70 group-data-[placeholder]/input:group-focus/input:text-primary',

      'group-focus/input:!pt-1 group-focus/input:!text-label-sm group-focus/input:text-primary',

      'group-data-[state=open]/input:pt-1 group-data-[state=open]/input:!text-label-sm group-data-[state=open]/input:text-primary',

      'group-disabled/input:!text-onSurfaceVariant/38 group-aria-[invalid=true]/input:!text-error',
      className
    )}
    {...props}
  />
))
FilledSelectLabel.displayName = 'FilledSelectLabel'

const OutlinedSelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
    error?: boolean
  }
>(({ className, children, error, ...props }, ref) => {
  return (
    <SelectPrimitive.Trigger
      ref={ref}
      aria-invalid={error ? 'true' : undefined}
      className={cn(
        'peer/input group/input relative z-0 h-14 w-full grow rounded-sm bg-transparent px-4 py-2 text-start text-body-lg text-onSurface outline-none transition-colors focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 data-[placeholder]:text-onSurfaceVariant/50',

        'before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:border before:border-outline before:transition-[color,border]',

        'focus:before:border-2 focus:before:border-primary has-[label]:focus:before:border-t-transparent has-[label]:focus:data-[placeholder]:before:border-t-transparent',

        'data-[state=open]:before:border-2 data-[state=open]:before:border-primary has-[label]:data-[state=open]:before:border-t-transparent has-[label]:data-[state=open]:data-[placeholder]:before:border-t-transparent',

        'has-[label]:before:border-t-transparent has-[label]:data-[placeholder]:before:border-t-outline',

        'disabled:before:border-onSurface/12 disabled:before:border-t-transparent has-[label]:disabled:before:border-transparent has-[label]:disabled:before:border-b-onSurface/12 has-[label]:disabled:before:border-t-transparent disabled:data-[placeholder]:before:border-onSurface/12',

        'has-[label]:data-[placeholder]:data-[state=open]:text-onSurfaceVariant/50 has-[label]:data-[placeholder]:text-transparent has-[label]:data-[placeholder]:focus:text-onSurfaceVariant/50',

        'aria-[invalid=true]:before:border-2 aria-[invalid=true]:before:border-error aria-[invalid=true]:before:border-t-transparent aria-[invalid=true]:data-[state=open]:before:border-error aria-[invalid=true]:has-[label]:data-[placeholder]:before:border-t-error aria-[invalid=true]:has-[label]:data-[placeholder]:data-[state=open]:before:border-t-transparent aria-[invalid=true]:has-[label]:data-[placeholder]:focus:before:border-t-transparent',
        className
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon
        asChild
        className="absolute right-2 top-2/4 grid -translate-y-2/4 place-items-center text-onSurfaceVariant/70 transition-transform group-disabled/input:text-onSurface/38 group-data-[state=open]/input:rotate-180 [&>i]:text-[20px] [&>svg]:h-5 [&>svg]:w-5"
      >
        <Icon symbol="expand_more" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
})
OutlinedSelectTrigger.displayName = 'OutlinedSelectTrigger'

const OutlinedSelectLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(
      'pointer-events-none absolute left-0 top-[-6px] z-10 mr-1 flex h-full w-full select-none text-label-sm font-normal leading-tight text-onSurfaceVariant duration-200',

      'group-data-[placeholder]/input:text-body-lg group-data-[placeholder]/input:leading-[4.2] group-data-[placeholder]/input:text-onSurfaceVariant/70',

      'group-focus/input:text-primary group-data-[placeholder]/input:group-focus/input:text-label-sm group-data-[placeholder]/input:group-focus/input:leading-tight group-data-[placeholder]/input:group-focus/input:text-primary',

      'group-data-[state=open]/input:text-label-sm group-data-[state=open]/input:leading-tight group-data-[state=open]/input:text-primary',

      'group-data-[placeholder]/input:group-data-[state=open]/input:text-label-sm group-data-[placeholder]/input:group-data-[state=open]/input:leading-tight group-data-[placeholder]/input:group-data-[state=open]/input:text-primary',

      'group-disabled/input:!text-onSurfaceVariant/38 group-data-[invalid]/container:text-error',

      'group-aria-[invalid=true]/input:!text-error',
      outlinedLabelBeforeAndAfter,
      className
    )}
    {...props}
  />
))
OutlinedSelectLabel.displayName = 'OutlinedSelectLabel'

const SelectSubheader = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => {
  return (
    <SelectPrimitive.Label
      ref={ref}
      className={cn(
        'px-3 py-2 text-label-md text-onSurfaceVariant/70',
        className
      )}
      {...props}
    />
  )
})
SelectSubheader.displayName = SelectPrimitive.Label.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        'relative z-50 max-h-80 min-w-[8rem] overflow-x-hidden rounded-md bg-surfaceContainer text-onSurfaceVariant shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
        position === 'popper' &&
          'data-[side=bottom]:translate-y-0 data-[side=left]:-translate-x-0 data-[side=right]:translate-x-0 data-[side=top]:-translate-y-0',
        className
      )}
      position={position}
      {...props}
    >
      <ScrollArea.Root className="h-full w-full" type="auto">
        <SelectPrimitive.Viewport
          asChild
          className={cn(
            'py-2',
            position === 'popper' &&
              'h-full w-full min-w-[var(--radix-select-trigger-width)]'
          )}
        >
          <ScrollArea.Viewport className="h-full w-full">
            {children}
          </ScrollArea.Viewport>
        </SelectPrimitive.Viewport>
      </ScrollArea.Root>
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      'group relative flex h-12 cursor-pointer select-none items-center rounded-none px-3 py-2 text-body-lg text-onSurface outline-none transition-colors hover:bg-onSurface/8 focus:bg-onSurface/8 active:bg-onSurface/12 aria-selected:bg-onSurface/8 data-[disabled]:pointer-events-none data-[state=checked]:bg-primary/12 data-[disabled]:text-onSurface/38',
      className
    )}
    {...props}
  >
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn('-mx-1 my-1 h-px bg-outlineVariant/38', className)}
    {...props}
  />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

const FilledSelect = Object.assign(FilledSelectRoot, {
  Group: SelectGroup,
  Value: SelectValue,
  Trigger: FilledSelectTrigger,
  Label: FilledSelectLabel,
  Content: SelectContent,
  Item: SelectItem,
  Separator: SelectSeparator,
  Subheader: SelectSubheader,
})

const OutlinedSelect = Object.assign(OutlinedSelectRoot, {
  Group: SelectGroup,
  Value: SelectValue,
  Trigger: OutlinedSelectTrigger,
  Label: OutlinedSelectLabel,
  Content: SelectContent,
  Item: SelectItem,
  Separator: SelectSeparator,
  Subheader: SelectSubheader,
})

export {
  FilledSelect,
  OutlinedSelect,
  FilledSelectTrigger,
  FilledSelectLabel,
  OutlinedSelectTrigger,
  OutlinedSelectLabel,
  SelectGroup,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectSeparator,
  SelectSubheader,
}

Update the import paths to match your project setup.

Examples

Filled

Default

import { FilledSelect as Select } from '@/components/ui/select'

export const FilledSelectDefault = () => {
  return (
    <Select>
      <Select.Trigger>
        <Select.Value placeholder="Select a city" />
      </Select.Trigger>
      <Select.Content>
        <Select.Group>
          <Select.Subheader>Andalucía</Select.Subheader>
          <Select.Item value="almeria">Almería</Select.Item>
          <Select.Item value="cadiz">Cádiz</Select.Item>
          <Select.Item value="cordoba">Córdoba</Select.Item>
          <Select.Item value="granada">Granada</Select.Item>
          <Select.Item value="huelva">Huelva</Select.Item>
          <Select.Item value="jaen">Jaén</Select.Item>
          <Select.Item value="malaga">Málaga</Select.Item>
          <Select.Item value="sevilla">Sevilla</Select.Item>
        </Select.Group>
      </Select.Content>
    </Select>
  )
}

With label

import { FilledSelect as Select } from '@/components/ui/select'

export const FilledSelectWithLabel = () => {
  return (
    <Select>
      <Select.Trigger>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}

With error

import { FilledSelect as Select } from '@/components/ui/select'

export const FilledSelectWithError = () => {
  return (
    <Select>
      <Select.Trigger error>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}

Disabled

import { FilledSelect as Select } from '@/components/ui/select'

export const FilledSelectDisabled = () => {
  return (
    <Select value="cordoba">
      <Select.Trigger disabled>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}

Oulined

Default

import { OutlinedSelect as Select } from '@/components/ui/select'

export const OutlinedSelectDefault = () => {
  return (
    <Select>
      <Select.Trigger>
        <Select.Value placeholder="Select a city" />
      </Select.Trigger>
      <Select.Content>
        <Select.Group>
          <Select.Subheader>Andalucía</Select.Subheader>
          <Select.Item value="almeria">Almería</Select.Item>
          <Select.Item value="cadiz">Cádiz</Select.Item>
          <Select.Item value="cordoba">Córdoba</Select.Item>
          <Select.Item value="granada">Granada</Select.Item>
          <Select.Item value="huelva">Huelva</Select.Item>
          <Select.Item value="jaen">Jaén</Select.Item>
          <Select.Item value="malaga">Málaga</Select.Item>
          <Select.Item value="sevilla">Sevilla</Select.Item>
        </Select.Group>
      </Select.Content>
    </Select>
  )
}

With label

import { OutlinedSelect as Select } from '@/components/ui/select'

export const OutlinedSelectWithLabel = () => {
  return (
    <Select>
      <Select.Trigger>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}

With error

import { OutlinedSelect as Select } from '@/components/ui/select'

export const OutlinedSelectWithError = () => {
  return (
    <Select>
      <Select.Trigger error>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}

Disabled

import { OutlinedSelect as Select } from '@/components/ui/select'

export const OutlinedSelectDisabled = () => {
  return (
    <Select value="cordoba">
      <Select.Trigger disabled>
        <Select.Value placeholder="Select a city" />
        <Select.Label>City</Select.Label>
      </Select.Trigger>
      <Select.Content>
        <SelectItems />
      </Select.Content>
    </Select>
  )
}