# Tag Select

> Colored tag selector with single or multiple selection and color variants.

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

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

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/tag-select.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`

## Files

### `registry/selection/tag-select.tsx`

````tsx
'use client'

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

/**
 * Represents an individual tag option.
 * @interface Tag
 * @property {string} id - Unique identifier for the tag
 * @property {string} label - Display text for the tag
 * @property {"default" | "blue" | "green" | "red" | "yellow" | "purple"} [color] - Optional color theme
 */
export interface Tag {
  id?: string
  label?: string
  color?: 'default' | 'blue' | 'green' | 'red' | 'yellow' | 'purple'
}

/**
 * ═══════════════════════════════════════════════════════════════════════════
 * TagSelectProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the TagSelect component, which provides tag-based filtering with
 * single or multiple selection modes and validation support.
 */
export interface TagSelectProps {
  data?: {
    /** Array of tags to display for selection. */
    tags?: Tag[]
  }
  actions?: {
    /** Called when the user clicks the validate button with the selected tag IDs. */
    onValidate?: (tagIds: string[]) => void
  }
  appearance?: {
    /**
     * Selection mode: single allows one tag, multiple allows many.
     * @default "multiple"
     */
    mode?: 'single' | 'multiple'
    /**
     * Whether to show the clear selection button.
     * @default true
     */
    showClear?: boolean
    /**
     * Whether to show the validate button.
     * @default true
     */
    showValidate?: boolean
    /**
     * Custom label for the validate button.
     * @default "Validate selection"
     */
    validateLabel?: string
  }
  control?: {
    /** Array of pre-selected tag IDs for controlled mode. */
    selectedTagIds?: string[]
  }
}


// ChatGPT-compliant: all tags use neutral system colors
const tagClasses = {
  selected: 'bg-foreground text-background border-foreground',
  unselected: 'bg-background text-foreground border-border hover:bg-muted'
}

/**
 * A tag selection component with single or multiple selection modes.
 * Features clear selection and validate buttons for user actions.
 *
 * Features:
 * - Single or multiple selection modes
 * - Check icon on selected tags
 * - Clear selection button with count
 * - Validate button with selection count
 * - Neutral system color scheme
 *
 * @component
 * @example
 * ```tsx
 * <TagSelect
 *   data={{
 *     tags: [
 *       { id: "1", label: "Electronics" },
 *       { id: "2", label: "Audio" },
 *       { id: "3", label: "Wireless" }
 *     ]
 *   }}
 *   actions={{
 *     onSelectTags: (ids) => console.log("Selected:", ids),
 *     onValidate: (ids) => console.log("Validated:", ids)
 *   }}
 *   appearance={{
 *     mode: "multiple",
 *     showClear: true,
 *     showValidate: true,
 *     validateLabel: "Apply filters"
 *   }}
 * />
 * ```
 */
export function TagSelect({ data, actions, appearance, control }: TagSelectProps) {
  const resolved: NonNullable<TagSelectProps['data']> = data ?? { tags: demoTags }
  const tags = resolved.tags ?? []
  const onValidate = actions?.onValidate
  const mode = appearance?.mode ?? 'multiple'
  const showClear = appearance?.showClear ?? true
  const showValidate = appearance?.showValidate ?? true
  const validateLabel = appearance?.validateLabel ?? 'Validate selection'
  const selectedTagIds = control?.selectedTagIds
  const [selected, setSelected] = useState<string[]>(selectedTagIds ?? [])

  // Sync internal state when controlled prop changes
  useEffect(() => {
    setSelected(selectedTagIds ?? [])
  }, [selectedTagIds])

  const handleToggle = (tagId: string) => {
    let newSelected: string[]

    if (mode === 'single') {
      newSelected = selected.includes(tagId) ? [] : [tagId]
    } else {
      newSelected = selected.includes(tagId)
        ? selected.filter((id) => id !== tagId)
        : [...selected, tagId]
    }

    setSelected(newSelected)
  }

  const handleClear = () => {
    setSelected([])
  }

  const handleValidate = () => {
    onValidate?.(selected)
  }

  const isSelected = (tagId: string) => selected.includes(tagId)

  return (
    <div className="w-full space-y-2 bg-card rounded-lg p-4">
      <div className="flex flex-wrap gap-2">
        {tags.map((tag, index) => {
          const tagId = tag.id ?? `tag-${index}`
          return (
            <button
              key={tagId}
              onClick={() => handleToggle(tagId)}
              className={cn(
                'inline-flex items-center gap-1 sm:gap-1.5 rounded-full border px-2.5 sm:px-3 py-0.5 sm:py-1 text-xs sm:text-sm transition-colors cursor-pointer',
                isSelected(tagId) ? tagClasses.selected : tagClasses.unselected
              )}
            >
              {isSelected(tagId) && (
                <Check className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
              )}
              {tag.label && <span>{tag.label}</span>}
            </button>
          )
        })}
      </div>

      <div className="flex items-center justify-between">
        {showClear && selected.length > 0 ? (
          <button
            onClick={handleClear}
            className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
          >
            <X className="h-3 w-3" />
            Clear selection ({selected.length})
          </button>
        ) : (
          <div />
        )}

        {showValidate && (
          <Button
            onClick={handleValidate}
            disabled={selected.length === 0}
            size="sm"
          >
            {validateLabel}
            {selected.length > 0 && ` (${selected.length})`}
          </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' },
]
```
