Tag Input
A text input that converts typed values into removable tags.
<!-- Tag Input -->
<!-- An Alpine.js and Tailwind CSS component by https://pinemix.com -->
<div
class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-zinc-200/75 bg-zinc-50 px-4 py-44 dark:border-zinc-700 dark:bg-zinc-950/25"
>
<div
x-data="{
// Customize Tag Input
maxTags: 0,
allowDuplicates: false,
// Helper variables
tags: [],
inputValue: '',
feedback: '',
feedbackTimeout: null,
// Add a tag
addTag() {
let tag = this.inputValue.trim();
if (tag === '') return;
if (!this.allowDuplicates && this.tags.includes(tag)) {
this.showFeedback('Duplicate tag');
this.inputValue = '';
return;
}
if (this.isMaxReached()) {
this.showFeedback('Maximum of ' + this.maxTags + ' tags reached');
return;
}
this.tags.push(tag);
this.inputValue = '';
this.feedback = '';
},
// Remove a tag by index
removeTag(index) {
this.tags.splice(index, 1);
this.feedback = '';
$nextTick(() => {
$refs.tagInput.focus();
});
},
// Remove last tag on backspace when input is empty
removeLastTag() {
if (this.inputValue === '' && this.tags.length > 0) {
this.tags.pop();
this.feedback = '';
}
},
// Check if max tags reached
isMaxReached() {
return this.maxTags > 0 && this.tags.length >= this.maxTags;
},
// Show feedback message
showFeedback(message) {
this.feedback = message;
clearTimeout(this.feedbackTimeout);
this.feedbackTimeout = setTimeout(() => {
this.feedback = '';
}, 2000);
}
}"
class="space-y-1"
>
<!-- Label -->
<label
for="pm-tag-input"
class="block text-sm font-medium text-zinc-900 dark:text-zinc-100"
>
Tags
</label>
<!-- END Label -->
<!-- Tag Input Container -->
<div
x-on:click="$refs.tagInput.focus()"
class="flex min-h-10 w-md flex-wrap items-center gap-1.5 rounded-lg border border-zinc-200 bg-white px-3 py-2 focus-within:border-zinc-500 focus-within:ring-3 focus-within:ring-zinc-500/50 dark:border-zinc-600 dark:bg-transparent dark:focus-within:border-zinc-500"
>
<!-- Tags -->
<template x-for="(tag, index) in tags" :key="index">
<span
x-init="
const isDark = document.documentElement.classList.contains('dark');
$el.animate(
[
{ backgroundColor: isDark ? 'var(--color-teal-900)' : 'var(--color-teal-200)' },
{}
],
{ duration: 500, easing: 'ease-out' }
)
"
class="inline-flex items-center gap-1 rounded-md bg-zinc-100 py-0.5 pr-1 pl-2 text-sm font-medium text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200"
>
<span x-text="tag"></span>
<button
x-on:click.stop="removeTag(index)"
type="button"
class="inline-flex items-center rounded p-0.5 text-zinc-400 hover:text-rose-600 focus:outline-hidden dark:text-zinc-400 dark:hover:text-rose-200"
aria-label="Remove tag"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="hi-micro hi-x-mark inline-block size-4"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
</button>
</span>
</template>
<!-- END Tags -->
<!-- Input -->
<input
x-ref="tagInput"
x-model="inputValue"
x-on:keydown.enter.prevent="addTag()"
x-on:keydown.backspace="removeLastTag()"
x-bind:placeholder="tags.length === 0 ? 'Type and press Enter to add tags..' : ''"
x-bind:class="{
'hidden': isMaxReached(),
}"
id="pm-tag-input"
type="text"
class="min-w-24 flex-1 border-none bg-transparent py-0 pr-0 pl-2 text-sm/6 placeholder-zinc-500 focus:ring-0 focus:outline-hidden disabled:cursor-not-allowed dark:placeholder-zinc-400"
/>
<!-- END Input -->
</div>
<!-- END Tag Input Container -->
<!-- Feedback Message -->
<p
x-cloak
x-show="feedback"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-2"
x-text="feedback"
class="text-sm font-medium text-amber-600 dark:text-amber-400"
></p>
<!-- END Feedback Message -->
</div>
</div>
<!-- END Tag Input -->
With max tags
<!-- Tag Input: With max tags -->
<!-- An Alpine.js and Tailwind CSS component by https://pinemix.com -->
<div
class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-zinc-200/75 bg-zinc-50 px-4 py-44 dark:border-zinc-700 dark:bg-zinc-950/25"
>
<div
x-data="{
// Customize Tag Input
maxTags: 5,
allowDuplicates: false,
// Helper variables
tags: [],
inputValue: '',
feedback: '',
feedbackTimeout: null,
// Add a tag
addTag() {
let tag = this.inputValue.trim();
if (tag === '') return;
if (!this.allowDuplicates && this.tags.includes(tag)) {
this.showFeedback('Duplicate tag');
this.inputValue = '';
return;
}
if (this.isMaxReached()) {
this.showFeedback('Maximum of ' + this.maxTags + ' tags reached');
return;
}
this.tags.push(tag);
this.inputValue = '';
this.feedback = '';
},
// Remove a tag by index
removeTag(index) {
this.tags.splice(index, 1);
this.feedback = '';
$nextTick(() => {
$refs.tagInput.focus();
});
},
// Remove last tag on backspace when input is empty
removeLastTag() {
if (this.inputValue === '' && this.tags.length > 0) {
this.tags.pop();
this.feedback = '';
}
},
// Check if max tags reached
isMaxReached() {
return this.maxTags > 0 && this.tags.length >= this.maxTags;
},
// Show feedback message
showFeedback(message) {
this.feedback = message;
clearTimeout(this.feedbackTimeout);
this.feedbackTimeout = setTimeout(() => {
this.feedback = '';
}, 2000);
}
}"
class="space-y-1"
>
<!-- Label and Counter -->
<div class="flex items-center justify-between">
<label
for="pm-tag-input-max"
class="block text-sm font-medium text-zinc-900 dark:text-zinc-100"
>
Tags
</label>
<span
class="text-xs"
x-bind:class="{
'text-zinc-500 dark:text-zinc-400': !isMaxReached(),
'text-amber-600 dark:text-amber-400': isMaxReached(),
}"
>
<span x-text="tags.length"></span> / <span x-text="maxTags"></span>
</span>
</div>
<!-- END Label and Counter -->
<!-- Tag Input Container -->
<div
x-on:click="$refs.tagInput.focus()"
class="flex min-h-10 w-md flex-wrap items-center gap-1.5 rounded-lg border bg-white px-3 py-2 focus-within:ring-3 dark:bg-transparent"
x-bind:class="{
'border-zinc-200 focus-within:border-zinc-500 focus-within:ring-zinc-500/50 dark:border-zinc-600 dark:focus-within:border-zinc-500': !isMaxReached(),
'border-amber-300 focus-within:border-amber-500 focus-within:ring-amber-500/50 dark:border-amber-600/75 dark:focus-within:border-amber-500': isMaxReached(),
}"
>
<!-- Tags -->
<template x-for="(tag, index) in tags" :key="index">
<span
x-init="
const isDark = document.documentElement.classList.contains('dark');
$el.animate(
[
{ backgroundColor: isDark ? 'var(--color-teal-800)' : 'var(--color-teal-200)' },
{}
],
{ duration: 500, easing: 'ease-out' }
)
"
class="inline-flex items-center gap-1 rounded-md bg-zinc-100 py-0.5 pr-1 pl-2 text-sm font-medium text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200"
>
<span x-text="tag"></span>
<button
x-on:click.stop="removeTag(index)"
type="button"
class="inline-flex items-center rounded p-0.5 text-zinc-400 hover:text-rose-600 focus:outline-hidden dark:text-zinc-400 dark:hover:text-rose-200"
aria-label="Remove tag"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="hi-micro hi-x-mark inline-block size-4"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
</button>
</span>
</template>
<!-- END Tags -->
<!-- Input -->
<input
x-ref="tagInput"
x-model="inputValue"
x-on:keydown.enter.prevent="addTag()"
x-on:keydown.backspace="removeLastTag()"
x-bind:placeholder="tags.length === 0 ? 'Type and press Enter to add tags..' : ''"
x-bind:class="{
'hidden': isMaxReached(),
}"
id="pm-tag-input-max"
type="text"
class="min-w-24 flex-1 border-none bg-transparent py-0 pr-0 pl-2 text-sm/6 placeholder-zinc-500 focus:ring-0 focus:outline-hidden disabled:cursor-not-allowed dark:placeholder-zinc-400"
/>
<!-- END Input -->
</div>
<!-- END Tag Input Container -->
<!-- Feedback Message -->
<p
x-cloak
x-show="feedback"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-2"
x-text="feedback"
class="text-sm font-medium text-amber-600 dark:text-amber-400"
></p>
<!-- END Feedback Message -->
</div>
</div>
<!-- END Tag Input: With max tags -->
Props
The available data properties for this component.
| Property | Default | Description |
|---|---|---|
| maxTags | 0 | Sets the maximum number of tags allowed, set to 0 for unlimited |
| allowDuplicates | false | If set to 'true', duplicate tags are allowed |
About this component
Designed with
Tailkit
2,000+ Tailwind CSS code snippets for HTML, React, Vue.js and Alpine.js. AI-powered development with MCP Server.
Unlock 15+ free templates
Join our pixelcave newsletter to get them now & we'll also keep you updated about any new Pinemix components!