Bootstrap Tag Input Field — Free HTML CSS JS Snippet

Bootstrap Tag Input Field · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Container itself is a real Bootstrap form-control, inheriting native border/focus-ring styling
Enter or comma both commit the current text as a new removable pill
Case-insensitive duplicate detection with a temporary inline error message
Backspace on an empty input removes the most recently added tag, not mid-typing text
New pills are inserted before the input element so the typing cursor stays visually anchored at the end
CSS.escape() used when looking up a pill by value, avoiding broken selectors from special characters
Clicking empty space anywhere in the field focuses the input, not just clicking the input itself
Blur commits any unsubmitted typed text as a tag instead of silently discarding it

About this UI Snippet

Bootstrap Tag Input Field — HTML, CSS & JavaScript

Screenshot of the Bootstrap Tag Input Field snippet rendered live

This snippet makes a single element, #bstagContainer, carry the real Bootstrap form-control class so it inherits Bootstrap's own border, padding, and focus-ring styling exactly like a normal text input — but instead of containing text directly, it holds a flex-wrapped row of tag pills plus one small borderless <input class="bstag-input"> at the end, styled with border: 0, outline: 0, and a transparent background so it visually disappears into the container and looks like part of the same field.

The core logic lives in addTag(): on Enter or comma (checked in a single keydown listener, with e.preventDefault() stopping the comma from being typed literally into the input), it trims the current input value, checks for a case-insensitive duplicate against the tags array with tags.some(t => t.toLowerCase() === value.toLowerCase()), and either silently rejects the duplicate — flashing a temporary #bstagError message via showDuplicateError(), which clears any previous pending setTimeout before scheduling a new one so rapid duplicate attempts don't cause the message to flicker off early — or pushes the value into tags and calls renderTag() to insert a new .bstag-pill span before the input element with container.insertBefore(pill, input). Inserting before the input, rather than appending to the end of the container, is what keeps the typing cursor visually anchored at the end of the tag row instead of jumping around as pills are added.

Backspace handling checks two conditions together: the key must be Backspace and the input must already be empty. That guard matters because without it, backspacing through actual typed characters would also delete a tag the instant the input hit zero characters mid-edit — instead, the tag-removal branch only fires when there's nothing left to delete from the text itself, so removeTag(tags[tags.length - 1]) runs only on a genuinely empty input, matching how chip inputs in real apps like email "To:" fields behave.

Each pill stores its own value in a data-value attribute, and removeTag() looks the matching pill up with CSS.escape(value) inside the attribute selector — necessary because a tag value could contain characters like quotes that would otherwise break a naively-interpolated CSS selector string. A delegated click listener on the container handles pill removal via .bstag-pill-remove, and separately, clicking empty space inside the container (not on a pill) focuses the hidden-looking input, so the whole field behaves like one clickable text box rather than requiring a pixel-precise click on the tiny input itself. Finally, a blur listener commits any unsubmitted typed text as a tag when focus leaves the field, so clicking away after typing a value without pressing Enter doesn't silently discard it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Hand this snippet's HTML, CSS, and JS to an AI coding assistant like Claude and ask it to add a hidden input that serializes the tag list for real form submission, or to add a max-tags limit that disables further input once reached. It's also worth asking for autocomplete suggestions drawn from a predefined tag list as the user types.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Bootstrap 5.3 tag input field, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js) — the outer container itself must be a genuine Bootstrap form-control element, not custom CSS made to resemble one.

Requirements:
- A container styled as a single Bootstrap form-control that holds both rendered tag pills and one small borderless text input at the end, so the whole thing looks like one unified field.
- Pressing Enter or typing a comma while text is in the input must convert that text into a removable pill (with an x button) inserted before the input, then clear the input for the next tag.
- Typing a tag that already exists (case-insensitively) must be rejected with a visible temporary error message, without creating a duplicate pill.
- Pressing Backspace only when the input is already empty must remove the most recently added tag — it must never interfere with normal text editing while there is still text in the input.
- Clicking anywhere in the empty space of the container (not directly on a pill or the input) must focus the input.
- Losing focus (blur) while there is unsubmitted text in the input must commit that text as a tag rather than discarding it.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="bstag-card">
    <label class="form-label small fw-semibold" for="bstagInput">Tags</label>
    <div class="form-control bstag-container d-flex flex-wrap align-items-center gap-1" id="bstagContainer">
      <input type="text" id="bstagInput" class="bstag-input" placeholder="Add a tag...">
    </div>
    <div class="small text-muted mt-1">Press Enter or comma to add a tag. Backspace removes the last one.</div>
    <div class="small text-danger mt-1 d-none" id="bstagError">That tag has already been added.</div>
  </div>
</div>

Step by step

How to Use

  1. 1
    Load the snippetAn empty Bootstrap-styled field appears with just a blinking cursor and placeholder text "Add a tag...".
  2. 2
    Type "design" and press EnterThe text becomes a gray rounded pill labeled "design" with an × button, and the input clears for the next tag.
  3. 3
    Type "css," with a trailing commaThe comma itself triggers tag creation immediately, without needing to press Enter.
  4. 4
    Type "design" again and press EnterA red message appears below the field noting the tag already exists, and no duplicate pill is created.
  5. 5
    Press Backspace with the input emptyThe most recently added pill is removed entirely, letting you delete tags without clicking their × buttons.
  6. 6
    Click anywhere in the empty space of the fieldThe cursor focuses into the hidden input immediately, even though you didn't click exactly on it.

Real-world uses

Common Use Cases

Blog post, article, or product tagging
A natural companion to bootstrap-contact-form-char-counter style content forms that need free-form categorization.
SEARCH
Search interfaces with keyword chips
Use the same pill pattern to let users build a multi-keyword search query visually, similar in purpose to bootstrap-responsive-navbar-search.
Email-style "To" or recipient fields
The Enter/comma-to-pill and backspace-to-remove behavior mirrors how recipient chip fields work in email clients.
Skills, interests, or attribute entry in profile forms
An alternative free-text entry mode to the fixed-option bootstrap-multi-select-dropdown when the list of possible values isn't predefined.
Learning contenteditable-adjacent input patterns
A clear example of compositing a container element and a nested plain input to fake a single unified field.

Got questions?

Frequently Asked Questions

This snippet keeps tags only in the in-memory tags array for the demo. For a real form, you would add a hidden input whose value is updated to tags.join(",") (or a JSON string) every time addTag() or removeTag() runs, so the final list submits with the rest of the form.

Yes. In React, hold tags in useState and map it to pill elements in JSX instead of manually creating DOM nodes with renderTag(); in Vue, use a reactive array with v-for over the pills; in Angular, keep tags as a component array property and use *ngFor, calling the same Enter/comma/backspace logic from (keydown) bindings.

Comparison is case-insensitive — typing "Design" after "design" is already added will be rejected, since addTag() lowercases both the new value and every existing tag before comparing them with Array.some().

No — the Backspace handler only removes the last tag when input.value is already an empty string, so it never interferes with normal character-by-character editing of the text currently being typed.

A blur listener checks whether the input still has unsubmitted trimmed text and, if so, commits it as a tag automatically via the same addTag() function used for Enter and comma, rather than losing it silently.

Replace the form-control class on the container with Tailwind utilities like flex flex-wrap items-center gap-1 rounded-md border px-2 py-1, and rebuild .bstag-pill as a Tailwind inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-sm span — all JS logic for adding, removing, and deduping tags stays exactly the same.