Tutorials / / 4 min read
How to create a tag input with Tailwind CSS: JavaScript and Alpine.js
Build a tag input with Tailwind CSS where Enter adds a removable tag chip, two ways: vanilla JavaScript or Alpine.js with x-model and x-for.
Let’s create a tag input: type something, hit Enter, and a removable tag pops into the list. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a tag input?
A tag input lets users add short labels to a list: type a tag, press Enter, and it appears as a chip with a remove button. They show up everywhere from social media platforms to content systems, because tagging makes content easier to organize, filter, and search.
Use cases
- Content tagging: label posts or products so they are easy to organize.
- Filtering and search: let users narrow results using the tags they add.
- Suggestions and autocomplete: speed up tag entry with smart hints.
- Tag clouds: visualize which topics come up most often.
The markup
id="tags-component": the parent element of the tag input and tag list.id="tags-input-wrapper": the wrapper element for the input field and existing tags.id="new-tag-input": the input field for adding new tags.id="tags-list": the list element for displaying existing tags.
Note: The classes are removed for clarity.
<div id="tags-component">
<!-- Tag Input -->
<div id="tags-input-wrapper">
<!-- Input Field -->
<input id="new-tag-input" />
<!-- Existing Tags -->
<div id="tags-list"></div>
</div>
</div>The script
- The
keydownlistener checks for Enter, prevents the default behavior, trims the value, and pushes the tag if it isn’t empty, then clears the input and re-renders. renderTags(): clears the list and rebuilds it, onedivper tag with aspanfor the text and a remove button with an SVG close icon.- Each remove button splices its tag out of the
tagsarray and callsrenderTags()again to update the list.
document.addEventListener("DOMContentLoaded", function () {
const newTagInput = document.getElementById("new-tag-input");
const tagsList = document.getElementById("tags-list");
let tags = [];
newTagInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
event.preventDefault();
const tag = newTagInput.value.trim();
if (tag) {
tags.push(tag);
newTagInput.value = "";
renderTags();
}
}
});
function renderTags() {
tagsList.innerHTML = "";
tags.forEach((tag, index) => {
const tagElement = document.createElement("div");
tagElement.className =
"inline-flex items-center gap-x-0.5 rounded-md bg-orange-50 px-2 py-1 text-xs font-medium text-orange-700 ring-1 ring-inset ring-orange-700/10";
const tagText = document.createElement("span");
tagText.textContent = tag;
const removeButton = document.createElement("button");
removeButton.className = "ml-2";
removeButton.innerHTML =
'<svg class="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>';
removeButton.addEventListener("click", function () {
tags.splice(index, 1);
renderTags();
});
tagElement.appendChild(tagText);
tagElement.appendChild(removeButton);
tagsList.appendChild(tagElement);
});
}
});The Alpine.js version
Same component, no separate script: the state lives in x-data.
x-data: defines the data of the component, thetagsarray and thenewTaginput value.x-model="newTag"with@keydown.enter.prevent: pushes the trimmed tag into the array and clears the input.x-for="(tag, index) in tags"with:key="index": loops through the tags, with the key uniquely identifying each one.x-text="tag": displays the tag’s text.@click="tags.splice(index, 1)": removes the tag when the close button is clicked.
Note: The classes are removed for clarity.
<div
x-data="{
tags: [],
addTag(tag) {
if (tag.trim() !== '') {
this.tags.push(tag.trim());
}
},
removeTag(index) {
this.tags.splice(index, 1);
}
}"
>
<!-- Tag Input -->
<div class="w-full" x-data="{ newTag: '', tags: [] }">
<!-- Input Field -->
<input
x-model="newTag"
@keydown.enter.prevent="
if (newTag.trim() !== '') {
tags.push(newTag.trim());
newTag = '';
}
"
type="text"
placeholder="..."
/>
<!-- Tags will be added here -->
<div>
<template x-for="(tag, index) in tags" :key="index">
<div>
<span x-text="tag"></span>
<button @click="tags.splice(index, 1)" class="ml-2">
<!--- Close Icon goes here -->
</button>
</div>
</template>
</div>
</div>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If Alpine is available, x-model and x-for handle the input and the rendering declaratively. The vanilla version builds the chips with createElement, which is more code but has zero dependencies.
Conclusion
That’s a tag input: an array, an Enter handler, and a remove button per chip. Remember to customize it and adapt it to your specific use case, and make it user-friendly and accessible.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza