# Option List

> Tag-style option selector with single or multiple selection modes.

A shadcn/ui block from the [@manifest](https://ui.manifest.build) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/manifest/option-list).

- Page, with a live preview: https://shadcn.exchange/r/manifest/option-list
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/manifest/option-list-light-e0bb0661af2009d1.png
- Registry item: https://ui.manifest.build/r/option-list.json
- Categories: selection
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/option-list.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `https://ui.manifest.build/r/manifest-types.json`

## Files

### `registry/selection/option-list.tsx`

````tsx
'use client'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Check } from 'lucide-react'
import { useEffect, useState } from 'react'

// Import types from shared types file to avoid circular dependencies
import type { Option } from './types'
// Re-export for backward compatibility
export type { Option } from './types'

import { demoOptions } from './demo/selection'


/**
 * ═══════════════════════════════════════════════════════════════════════════
 * OptionListProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the OptionList component, which displays selectable options with
 * support for single or multiple selection modes.
 */
export interface OptionListProps {
  data?: {
    /** Array of selectable options to display. */
    options?: Option[]
  }
  actions?: {
    /** Called when the user confirms their selection. Returns selected option(s). */
    onSubmit?: (selected: Option[]) => void
  }
  appearance?: {
    /**
     * Enable multiple selection mode.
     * @default false
     */
    multiple?: boolean
  }
  control?: {
    /** Controlled selected index for single selection mode. */
    selectedOptionIndex?: number
    /** Controlled selected indexes for multiple selection mode. */
    selectedOptionIndexes?: number[]
  }
}

/**
 * An option list component with single or multiple selection.
 * Uses compact pill/chip design with check indicators.
 *
 * Features:
 * - Single and multiple selection modes
 * - Optional descriptions and icons
 * - Disabled state support
 * - Controlled and uncontrolled usage
 *
 * @component
 * @example
 * ```tsx
 * <OptionList
 *   data={{
 *     options: [
 *       { label: "Option A" },
 *       { label: "Option B", description: "With description" },
 *       { label: "Option C", disabled: true }
 *     ]
 *   }}
 *   appearance={{ multiple: true }}
 *   actions={{ onSelectOptions: (opts) => console.log(opts) }}
 * />
 * ```
 */
export function OptionList({ data, actions, appearance, control }: OptionListProps) {
  const resolved: NonNullable<OptionListProps['data']> = data ?? { options: demoOptions }
  const options = resolved.options ?? []
  const onSubmit = actions?.onSubmit
  const multiple = appearance?.multiple ?? false
  const selectedOptionIndex = control?.selectedOptionIndex
  const selectedOptionIndexes = control?.selectedOptionIndexes
  const [selected, setSelected] = useState<number | number[]>(
    multiple ? (selectedOptionIndexes ?? []) : selectedOptionIndex ?? -1
  )

  // Sync internal state when controlled props change
  useEffect(() => {
    if (multiple) {
      setSelected(selectedOptionIndexes ?? [])
    } else if (selectedOptionIndex !== undefined) {
      setSelected(selectedOptionIndex)
    }
  }, [multiple, selectedOptionIndex, selectedOptionIndexes])

  const handleSelect = (option: Option, index: number) => {
    if (option.disabled) return

    if (multiple) {
      const currentSelected = selected as number[]
      const newSelected = currentSelected.includes(index)
        ? currentSelected.filter((i) => i !== index)
        : [...currentSelected, index]
      setSelected(newSelected)
    } else {
      setSelected(index)
    }
  }

  const handleSubmit = () => {
    if (multiple) {
      const selectedIndexes = selected as number[]
      onSubmit?.(options.filter((_, i) => selectedIndexes.includes(i)))
    } else {
      const selectedIndex = selected as number
      if (selectedIndex >= 0) {
        onSubmit?.([options[selectedIndex]])
      }
    }
  }

  const isSelected = (index: number) => {
    if (multiple) {
      return (selected as number[]).includes(index)
    }
    return selected === index
  }

  const hasSelection = multiple
    ? (selected as number[]).length > 0
    : (selected as number) >= 0

  return (
    <div className="w-full bg-card rounded-lg p-4 space-y-3">
      <div className="flex flex-wrap gap-2">
        {options.map((option, index) => (
          <button
            key={option.label || index}
            onClick={() => handleSelect(option, index)}
            disabled={option.disabled}
            className={cn(
              'inline-flex items-center gap-1.5 sm:gap-2 rounded-full border px-2.5 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm transition-colors cursor-pointer',
              isSelected(index)
                ? 'border-foreground bg-foreground text-background'
                : 'border-border bg-background hover:bg-muted',
              option.disabled && 'opacity-50 !cursor-not-allowed'
            )}
          >
            {option.icon}
            {option.label && <span>{option.label}</span>}
            {option.description && (
              <span
                className={cn(
                  'text-[10px] sm:text-xs',
                  isSelected(index)
                    ? 'text-background/70'
                    : 'text-muted-foreground'
                )}
              >
                · {option.description}
              </span>
            )}
            {isSelected(index) && multiple && (
              <Check className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
            )}
          </button>
        ))}
      </div>
      {onSubmit && (
        <div className="flex justify-end">
          <Button
            size="sm"
            onClick={handleSubmit}
            disabled={!hasSelection}
          >
            Confirm
          </Button>
        </div>
      )}
    </div>
  )
}
````

### `registry/selection/demo/selection.ts`

```ts
// Demo data for Selection category components
// This file contains sample data used for component previews and documentation

import type { Option } from '../types'

// Default options for OptionList
export const demoOptions: Option[] = [
  { label: 'Standard shipping', description: '3-5 business days' },
  { label: 'Express shipping', description: '1-2 business days' },
  { label: 'Store pickup', description: 'Available in 2h' },
]

// Quick reply options
export const demoQuickReplies = [
  { label: 'Yes, please' },
  { label: 'No, thanks' },
  { label: 'Tell me more' },
]

// Tag select options
export const demoTags: { id: string; label: string; color: 'red' | 'yellow' | 'green' }[] = [
  { id: '1', label: 'Important', color: 'red' },
  { id: '2', label: 'In Progress', color: 'yellow' },
  { id: '3', label: 'Done', color: 'green' },
]
```
