webdevpuneet.com

Bootstrap Tag Input Field — Free HTML CSS JS Snippet

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

CategoryForms

Copy & paste

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>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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.

Design your website