Forms
Tags input
A field that turns typed or pasted text into removable chips, with duplicate feedback, a maximum, per-tag validation and a normal form value.
A tags input collects a short list of free-text values: labels, keywords, email addresses to invite. People type a value and press Enter or comma; it becomes a chip. When the values must come from a known list, use a Multi-select. For one free-text value, use an Input.
Examples
Basic
Put data-ds-tags on a text input. Without JavaScript it is a comma-separated text field. With ds.js the same input becomes hidden and keeps the comma-separated value, so the post is unchanged (Labels=design,urgent).
Press Enter or comma after each label.
View HTML code
<div class="ds-field" style="max-width: 30rem">
<label for="tg-labels">Labels</label>
<input id="tg-labels" name="Labels" class="input" value="finance, q3 close" data-ds-tags placeholder="Add a label" aria-describedby="tg-labels-hint">
<p id="tg-labels-hint" class="ds-hint">Press Enter or comma after each label.</p>
</div>Maximum and length
data-ds-tags-max caps the number of tags and shows a counter; data-ds-tags-maxlength trims each tag.
View HTML code
<div class="ds-field" style="max-width: 30rem">
<label for="tg-keywords">Search keywords</label>
<input id="tg-keywords" name="Keywords" class="input" value="invoice,overdue,PRJ-1024" data-ds-tags data-ds-tags-max="5" data-ds-tags-maxlength="24" placeholder="Add a keyword">
</div>Validation per tag
data-ds-tags-pattern is a regular expression each tag must match (whole value). Invalid tags stay as error chips so they can be removed or retyped, and the field reports "2 entries are not valid." on submit.
Paste a list separated by commas, semicolons or new lines.
View HTML code
<div class="ds-field" style="max-width: 30rem">
<label for="tg-invite">Invite by email</label>
<input id="tg-invite" name="Invitees" class="input" value="[email protected], daniel.okafor@example" data-ds-tags data-ds-tags-pattern="[^@\s]+@[^@\s]+\.[^@\s]+" placeholder="[email protected]" aria-describedby="tg-invite-hint">
<p id="tg-invite-hint" class="ds-hint">Paste a list separated by commas, semicolons or new lines.</p>
</div>One value per tag
data-ds-tags-mode="multiple" posts each tag as its own value under the same name (Tags=a&Tags=b), which binds to List<string>.
View HTML code
<div class="ds-field" style="max-width: 30rem">
<label for="tg-multi">Tags</label>
<input id="tg-multi" name="Tags" class="input" value="onboarding,remote" data-ds-tags data-ds-tags-mode="multiple">
</div>States
Remove or correct not-an-email.
View HTML code
<div class="grid gap-4 sm:grid-cols-2">
<div class="ds-field"><label for="tg-st-1">Empty</label><input id="tg-st-1" class="input" data-ds-tags placeholder="Add a label"></div>
<div class="ds-field"><label for="tg-st-2">Limit reached</label><input id="tg-st-2" class="input" value="north,south" data-ds-tags data-ds-tags-max="2"></div>
<div class="ds-field"><label for="tg-st-3">Invalid</label><input id="tg-st-3" class="input" value="[email protected],not-an-email" data-ds-tags data-ds-tags-pattern="[^@\s]+@[^@\s]+\.[^@\s]+" aria-invalid="true" aria-describedby="tg-st-3-err"><p id="tg-st-3-err" class="ds-field-error">Remove or correct not-an-email.</p></div>
<div class="ds-field"><label for="tg-st-4">Disabled</label><input id="tg-st-4" class="input" value="archived,2025" data-ds-tags disabled></div>
</div>| State | Trigger | Appearance |
|---|---|---|
| Empty | no tags | placeholder in --ds-subtle |
| Focus | focus in the entry | field focus ring (from Input) |
| Typing | text in the entry | text only; nothing is added until Enter, comma, Tab, paste or blur |
| Added | Enter, comma, Tab, blur | chip fades and scales in from 96% (160ms); " added." announced |
| Duplicate | the value already exists (case-insensitive) | existing chip flashes a primary tint for 1.6s; " is already in the list." |
| Limit reached | data-ds-tags-max |
counter in full text colour, further values refused with "You can add up to 5." |
| Invalid tag | value fails data-ds-tags-pattern |
chip in --ds-error-soft with error border and text; field invalid until fixed |
| Chip focus | Arrow Left from the entry | focus ring on the chip's × button |
| Removed | × button, Backspace, Delete | chip disappears, " removed." announced |
| Invalid field | aria-invalid="true" |
error border, message below |
| Disabled | disabled |
field dimmed, chips without remove action |
Anatomy
.ds-tags-input— wrapper (generated)..ds-tags-input__field.input— field around chips and entry..ds-tags-input__tokens— list of.ds-tokenchips..ds-tags-input__input— text entry; takes the originalid, so the label still points at it.input[type=hidden][data-ds-tags]— the original input (id{id}-value), holding the joined value..ds-tags-input__meta/.ds-tags-input__count— counter when a maximum is set.
Usage
- Say how to add a tag in the hint ("Press Enter or comma after each label.").
- Keep tags short (under 24 characters) and set
data-ds-tags-maxlengthto enforce it. - Accept paste of whole lists; people often copy from spreadsheets or email.
- Normalise on the server too (trim, case): the client prevents obvious duplicates, not all.
- Do not use for values that must exist in a list (people, projects); use a Multi-select.
Accessibility
- The entry is a normal text input with the field's label, hint and error. Chips are a list; each remove button is named "Remove ".
- Remove buttons are not Tab stops; Arrow Left at the start of the entry moves to them, so a field with 20 tags is still one Tab stop.
- A polite live region announces added, removed, duplicate, maximum and invalid values.
- Invalid chips use border, fill and text colour; the error message names the problem in words.
| Key | Action |
|---|---|
| Enter or , (comma) | Add the typed text as a tag (Enter with an empty entry submits the form) |
| Tab | Add the typed text, then move on |
| Backspace | With an empty entry: remove the last tag |
| Arrow Left | At the start of the entry: focus the last tag's remove button |
| Arrow Left / Right on a tag | Move between tags; past the last, back to the entry |
| Backspace / Delete / Enter on a tag | Remove that tag |
| Escape or End on a tag | Back to the entry |
| Ctrl/Cmd + V | Paste; text is split on commas, semicolons, tabs and new lines |
Razor
<div class="ds-field">
<label asp-for="Labels">Labels</label>
<ds-tags-input asp-for="Labels" max="8" max-length="24" placeholder="Add a label" />
</div>
<ds-tags-input asp-for="Invitees" pattern="[^@\s]+@[^@\s]+\.[^@\s]+" />A string property posts one comma-separated value; a collection (List<string>, string[]) switches to one value per tag. Other attributes: separator, required, disabled, size="sm". Model errors add aria-invalid and a message.
JavaScript
| Attribute | Effect |
|---|---|
input[data-ds-tags] |
Enhances the input |
data-ds-tags-separator |
Separator for the posted value and typing (default ,) |
data-ds-tags-mode="multiple" |
One hidden input per tag under the same name |
data-ds-tags-max |
Maximum number of tags (shows a counter) |
data-ds-tags-maxlength |
Maximum characters per tag |
data-ds-tags-pattern |
Regular expression each tag must match |
data-ds-tags-commit="false" |
Do not add the typed text on blur |
API: DS.tagsInput(input) returns { add(text), remove(tag), clear(), get() }. Events on the original input: input, change and ds:tags:change { tags }. Form reset restores the initial tags.
Tokens
Field: as Input. Chips: --color-base-200, --color-base-300, --ds-radius-md, --ds-radius-sm, --ds-type-footnote-size, --ds-subtle, --ds-error-soft, --ds-error-text, --color-primary, --ds-opacity-state-drag (duplicate flash), --ds-duration-quick, --ds-ease-emphasized.
Native and daisyUI
Builds on a text <input>, so it works without JavaScript as comma-separated text. The field is daisyUI .input used as a container; chips are the DS .ds-token, shared with Multi-select.