Prisma
Live Preview
React TypeScript Next.js

--- description: Hướng dẫn Agent tạo Tag Input — input tạo nhiều tags/chips

Tag Input / Multi Input Component

Skill này hướng dẫn tạo component Tag Input — input cho phép tạo nhiều tags/chips, với autocomplete, validation per-tag, và keyboard interaction.

1. Mục tiêu (Objective)

Tạo Tag Input: type + Enter tạo tag chip, backspace xóa, autocomplete suggestions. Composite component kết hợp input + chips.

2. AI Context & Intent (Ngữ cảnh cho AI)

Khi nào dùng Tag Input?

  • Email recipients: Nhập nhiều email addresses (To, CC, BCC)
  • Labels / Tags: Gán multiple labels cho items (GitHub issues, Jira)
  • Search filters: Multi-keyword search, multi-category filter
  • Skills / Interests: Select nhiều kỹ năng, sở thích

⚠️ Phân biệt Tag Input vs Select vs Checkbox Group (QUAN TRỌNG)

Tiêu chí Tag Input Multi Select Checkbox Group
Bản chất Free-text + chips Predefined options Fixed options
Input User types new values Choose from dropdown Toggle checkboxes
Custom values ✅ User creates new ❌ Fixed list only ❌ Fixed list only
Best for Open-ended multi-values Known option set 2-7 visible options
Ví dụ Email recipients Assign roles Privacy settings

Decision Tree cho AI

text
Cần nhập multiple values?
├─ User tự nhập values mới → Tag Input
│   ├─ Email addresses → Tag Input + email validation
│   ├─ Free labels → Tag Input + autocomplete
│   └─ Search keywords → Tag Input (simple, no suggestions)
│
├─ Chọn từ danh sách cố định → Multi Select
│   ├─ 5+ options → Select (multi mode)
│   └─ 2-5 options visible → Checkbox Group
│
├─ Single value selection → Radio / Single Select
└─ Yes/No toggle → Switch / Checkbox

3. Anatomy

text
┌──────────────────────────────────────────────────────┐
│  [admin ✕] [editor ✕] [viewer ✕]  Type to add...│  │ ← Container (--input bg)
│  ↑ chips    ↑ close    ↑           ↑ input field  │  │    --input-border
│  │          │          │           └─ placeholder  │  │    --comp-radius
│  │          │          └─ chip style: --primary-muted │
│  │          └─ close icon (x): --muted-foreground  │  │
│  └─ tags: --radius-full                            │  │
└──────────────────────────────────────────────────────┘
        │ type "des"
        ▼
┌──────────────────────────────────────────────────────┐
│  designer                                            │ ← Suggestion item
│  developer                                           │ ← --state-layer-hover
│  ─── Create "des" ───                                │ ← Create new option
└──────────────────────────────────────────────────────┘

4. Platform Mapping

Platform Component Key Difference
Web Tag Input (custom) Chips inside input wrapper
iOS 📎 UITextField + custom chips No native multi-value input
Android (M3) ChipGroup + TextField Material Chips, FilterChip
Fluent TagPicker Full-featured picker with suggestions
Carbon Tag + TextInput Separate components composed

Cross-platform note: Not a single native component anywhere — always composite. Token file covers both the container and individual tag chips.

4.4. Slot Map (Figma ↔ Code)

📎 Source: slot-manifest.jsontag-input · Layer: item

Figma Slot data-slot CSS Class Required Accepts
Root tag-input .tag-input
Tag tag-input-tag ❌ (n×)
Tag Text tag-input-tag-text text
Tag Remove tag-input-tag-remove close-icon
Field tag-input-field text-input

5. Token Mapping

📦 Atomic Mapping: See ATOMIC-MAPPING.md for complete token spec.

📦 Atomic Mapping: Tag Input dùng tokens từ input (container) + chip (chips) — UI Layer tùy theo context.

Container dùng --input / --input-border, chips dùng --primary-muted / --primary-muted-foreground, close icon dùng --muted-foreground. Component token JSON files đã deprecated.

6. Props & API

typescript
interface TagInputProps {
  value?: string[];
  placeholder?: string;
  maxTags?: number;
  suggestions?: string[];
  allowDuplicates?: boolean;
  validateTag?: (tag: string) => boolean | string;
  onAdd: (tag: string) => void;
  onRemove: (tag: string) => void;
  onChange: (tags: string[]) => void;
}

7. Accessibility (a11y)

  • Container: role="listbox" or custom, announces tag count.
  • Tags: role="option", close button aria-label="Remove [tag]".
  • Input: Standard text input, aria-describedby links to tag count.
  • Keyboard: Backspace removes last tag, Enter adds, Escape clears input.

8. Best Practices & Rules

  • Validation: Validate per-tag (email format, uniqueness).
  • Max tags: Show "(2 of 5)" when limit exists.
  • Overflow: Scroll container when many tags, or wrap with max-height.
  • Autocomplete: Debounced suggestion list, highlight matched text.
  • Không Hardcode: Mọi giá trị từ Token.
  • Icon: Close icon (x) CHỈ dùng Lucide icon từ assets/icons/. CẤM dùng text emoji. Xem icon.md mục 10.

9. Example Usage

jsx
{/* Email recipients */}
<TagInput
  value={recipients}
  placeholder="Add email..."
  validateTag={(tag) => /^[^@]+@[^@]+$/.test(tag) || "Invalid email"}
  onChange={setRecipients}
  onAdd={addRecipient}
  onRemove={removeRecipient}
/>

{/* Labels with autocomplete */}
<TagInput
  value={labels}
  placeholder="Add label..."
  suggestions={availableLabels}
  maxTags={5}
  onChange={setLabels}
  onAdd={addLabel}
  onRemove={removeLabel}
/>

{/* Search keywords */}
<TagInput
  value={keywords}
  placeholder="Add search keywords..."
  onChange={setKeywords}
  onAdd={addKeyword}
  onRemove={removeKeyword}
/>