TagInput
Enter a list of tags, keywords or emails: add with Enter or a comma, paste, suggestions, validation and inline editing.
- React
- TypeScript
'use client';
import { TagInput } from '@akhatmirzo/ui';
export default function TagInputBasic() {
return (
<div className="w-full max-w-md">
<TagInput
defaultValue={['React', 'TypeScript']}
placeholder="Teg qo'shing"
aria-label="Teglar"
/>
</div>
);
}
Import
import { TagInput } from '@akhatmirzo/ui';When to use
- A list of free-form values — tags, keywords, skills, several emails or phone numbers.
- When likely values are known — pass
suggestions: a list appears while typing, but users can still add their own value. - When users must pick only from a list — use
Comboboxwithmultiple; it doesn't let users create new values. - For a single value, use a plain
Input.
With Field
Inside a Field the label points at the inner <input>, and the description and
error go into aria-describedby. value + onValueChange is the controlled mode, defaultValue
the uncontrolled one.
Enter yoki vergul bilan qo'shing
- dizayn
- frontend
Qiymat: ["dizayn","frontend"]
'use client';
import { Field, TagInput } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function TagInputWithField() {
const [tags, setTags] = useState(['dizayn', 'frontend']);
return (
<div className="flex w-full max-w-md flex-col gap-3">
<Field label="Kalit so'zlar" description="Enter yoki vergul bilan qo'shing">
<TagInput value={tags} onValueChange={setTags} placeholder="Kalit so'z" />
</Field>
<p className="m-0 text-sm text-text-muted">
Qiymat: <span className="font-mono text-text">{JSON.stringify(tags)}</span>
</p>
</div>
);
}
Adding and removing
Enteror,turns the typed text into a tag (change this withdelimiters, e.g.['Enter', ',', 'Tab']). On an empty inputEntersubmits the form as usual.- Paste:
"a, b, c"→ 3 tags. Text is split on single-character delimiters and, whenEnteris a delimiter, on new lines. - Duplicates are rejected — the comparison ignores case (allow them with
allowDuplicates). transformturns raw text into a tag (default —trim).- Double-click a tag (or press
Enter/F2while it's focused) to edit it in place:Entersaves,Esccancels, clearing the text removes the tag.
Suggestions
With suggestions, matching options show up while typing. Matching uses normalizeUz: Cyrillic and
Latin script and apostrophe variants are treated the same — typing «тош» finds «Toshkent». Tags that
are already added are hidden. Move with ↓ / ↑, Enter adds; when nothing is highlighted,
Enter adds the typed text itself.
maxTags caps the number of tags. Once the limit is reached the input hides and the container gets
data-full.
Ko'pi bilan 4 ta. «тош» deb yozib ko'ring
'use client';
import { Field, TagInput } from '@akhatmirzo/ui';
const cities = ['Toshkent', 'Samarqand', 'Buxoro', "Farg'ona", 'Namangan', 'Andijon', 'Xiva'];
export default function TagInputSuggestions() {
return (
<div className="w-full max-w-md">
<Field label="Shaharlar" description="Ko'pi bilan 4 ta. «тош» deb yozib ko'ring">
<TagInput suggestions={cities} maxTags={4} placeholder="Shahar yozing" />
</Field>
</div>
);
}
Validation
validate runs for every new tag: true accepts it, false shows the default error
(labels.invalid), a string shows that string. A rejected tag isn't added, the text stays in the
input, the error appears below it and is announced to screen readers, and the input gets
aria-invalid. The error clears on the next change.
Bir nechta emailni vergul bilan qo'ying
'use client';
import { isValidEmail } from '@akhatmirzo/core';
import { Field, TagInput } from '@akhatmirzo/ui';
export default function TagInputValidate() {
return (
<div className="w-full max-w-md">
<Field label="Qabul qiluvchilar" description="Bir nechta emailni vergul bilan qo'ying">
<TagInput
validate={(email) => isValidEmail(email) || "Email noto'g'ri"}
transform={(raw) => raw.trim().toLowerCase()}
placeholder="Email qo'shing"
/>
</Field>
</div>
);
}
The validation error is separate from the Field error prop — TagInput renders it itself. Pass
form-level errors (e.g. "At least one tag is required") through Field error.
Sizes and states
size — sm, md (default), lg: the same minimum height as Input (32 / 40 / 48px); the
container grows as tags wrap. In disabled and readOnly states the ✕ buttons are hidden.
- sm
- md
- lg
- O'chirilgan
'use client';
import { TagInput } from '@akhatmirzo/ui';
export default function TagInputSizes() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<TagInput size="sm" defaultValue={['sm']} aria-label="Kichik" />
<TagInput defaultValue={['md']} aria-label="O'rtacha" />
<TagInput size="lg" defaultValue={['lg']} aria-label="Katta" />
<TagInput disabled defaultValue={["O'chirilgan"]} aria-label="O'chirilgan" />
</div>
);
}
Forms
With name, every tag gets a hidden <input> with that name:
new FormData(form).getAll('tags') → ['React', 'Vue']. For PHP-style arrays use
name="tags[]".
Props
className goes to the root container; ref and other <input> attributes (placeholder,
autoFocus, onBlur ...) go to the inner input.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string[] | — | Tags (controlled) |
defaultValue | string[] | [] | Initial tags |
onValueChange | (value: string[]) => void | — | Called when the list changes |
delimiters | string[] | ['Enter', ','] | Keys and characters that finish a tag ('Tab' is allowed too) |
maxTags | number | — | Maximum number of tags |
allowDuplicates | boolean | false | Allow duplicate tags (compared case-insensitively) |
validate | (tag: string) => boolean | string | — | false or a message — the tag is rejected and the error is shown |
transform | (raw: string) => string | trim | Turns raw text into a tag |
suggestions | string[] | — | Suggestions shown while typing (script-insensitive) |
size | 'sm' | 'md' | 'lg' | 'md' | Minimum height 32 / 40 / 48px |
disabled | boolean | — | Disabled state |
readOnly | boolean | — | Read-only: no adding or removing |
invalid | boolean | — | Error state (automatic inside Field) |
name | string | — | A hidden input with this name for every tag |
labels | { remove?; suggestions?; added?; removed?; invalid?; duplicate? } | — | Built-in texts (Uzbek by default) — see the table below |
inputClassName | string | — | Class for the inner `<input>` |
labels
| Prop | Type | Default | Description |
|---|---|---|---|
remove | (tag: string) => string | `${tag} ni olib tashlash` | Name of the ✕ button |
suggestions | string | 'Takliflar' | Name of the suggestion list |
added | (tag: string) => string | `${tag} qo'shildi` | Screen reader announcement |
removed | (tag: string) => string | `${tag} olib tashlandi` | Screen reader announcement |
invalid | (tag: string) => string | `«${tag}» qabul qilinmadi` | When `validate` returns false |
duplicate | (tag: string) => string | `«${tag}» allaqachon qo'shilgan` | Duplicate tag |
Data attributes
| Attribute | Where | Value |
|---|---|---|
data-size | container | sm · md · lg |
data-full | container | present when maxTags is hit |
data-invalid | container | error (or validation error) |
data-disabled | container | present when disabled |
data-readonly | container | present when readOnly |
data-slot | parts | tag-input · tag-input-root |
Keyboard
| Key | Action |
|---|---|
Enter / , | Turn the typed text into a tag |
Backspace (empty input) | Select the last tag; press again to remove it |
← (at input start) | Move to the last tag |
← / → (on a tag) | Move between tags; → on the last one returns to input |
Backspace / Delete (on a tag) | Remove the tag |
Enter / F2 (on a tag) | Edit the tag |
Esc (on a tag) | Back to the input |
↓ / ↑ | Move through suggestions (with suggestions) |
Esc | Close the suggestion list |
Accessibility
- List. Tags live in a
<ul role="list">— screen readers announce how many there are. - Announcements. Adding and removing are announced in a hidden
aria-live="polite"region ("React qo'shildi", "React olib tashlandi"); customize withlabels.added/labels.removed. - Errors. The validation error is visible text in a live region, linked to the input via
aria-invalidandaria-describedby. - Suggestions. With
suggestionsthe input is arole="combobox"(aria-expanded,aria-controls,aria-activedescendant) and the list is arole="listbox". Focus always stays in the input. - ✕ buttons have
aria-label="React ni olib tashlash"and a hit area of at least 40px. - Tags are focused with the arrow keys (
tabIndex={-1}); the ✕ buttons are also reachable withTab.