Multi-Select Dropdown — Free HTML CSS JS Snippet

Multi-Select Dropdown · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Selected items render as removable tag chips with × button inside the trigger
Custom CSS checkbox: .selected class + ::after L-shape checkmark — no SVG needed
Inline search: filterOptions() hides non-matching options, preserves selected state
Selected Set: O(1) has/add/delete, no duplicates, iterable for tag rendering
Select all: iterates OPTIONS array and adds all to Set
Clear all: selected.clear() + resets search input and query
Click-outside close: document listener + closest() check
Chip count below trigger: "N of M selected" label

About this UI Snippet

Multi-Select Dropdown — Tag Chips, Inline Search, Custom Checkboxes & Select All

Screenshot of the Multi-Select Dropdown snippet rendered live

A multi-select dropdown lets users choose multiple items from a list — selected items appear as removable tag chips inside the trigger button. It is the multi-value cousin of the custom select, needed in filter panels, tag management systems, settings pages, and any form where users need to select several options from a larger set. This snippet provides a complete implementation with tag chips, inline search filter, custom CSS checkboxes, select-all, clear-all, and click-outside close — all in plain HTML, CSS, and vanilla JavaScript without any library.

The tag chip display

Selected items render as .tag spans inside the trigger button — the same chips as the tag input. Each chip shows the option icon and label, plus a × remove button that calls toggle(value) with stopPropagation to prevent the dropdown from toggling. As chips are added, the trigger's min-height: 42px expands to fit multiple lines of chips naturally via flex-wrap: wrap.

The custom CSS checkbox

Each option has a .ms-checkbox div that uses a CSS ::after pseudo-element for the checkmark when .selected is applied. The checkmark uses a rotated L-shape: width: 8px, height: 5px, with border-left and border-bottom in white, rotated -45 degrees. No SVG, no icon font — pure CSS.

Inline search filtering

The search input at the top of the dropdown calls filterOptions(query) on every keypress. This adds .hidden to any option whose label does not include the query string (case-insensitive). Filtered options are hidden via display: none but their selected state is preserved — filtering does not deselect items.

Select all and Clear all

The footer buttons iterate the OPTIONS array: selectAll() adds every value to the selected Set; clearAll() calls selected.clear() and resets the search input and query. Both re-render tags and options immediately.

The selected Set

JavaScript's Set data structure is ideal for multi-select state — it prevents duplicates automatically, provides O(1) has() lookup, and supports delete() and clear() operations. The Set is converted to iteration with forEach and filter when building the tags and options display.

Accessibility

The trigger has role="combobox" with aria-expanded and aria-haspopup="listbox". The dropdown has role="listbox" and aria-multiselectable="true". Each option has role="option" and aria-selected toggling. The search input receives focus automatically when the dropdown opens.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than tracing the Set-based state by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why a JavaScript Set is used for the selected state instead of an array, and how toggle(), renderTags(), and renderOptions() stay consistent with each other across every entry point (option click, tag remove, select all, clear all). The same assistant can help optimize it, for instance asking whether re-running renderOptions() over the full OPTIONS array on every keystroke of the search field would still be fast with a list of several hundred items, or whether the click-outside document listener could interfere with other dropdowns on the same page. It's also useful for extending the component: ask it to add a maximum-selections limit with a friendly inline warning, group options under category headers within the dropdown, or persist the selected Set to the URL or localStorage so the choice survives a refresh. Treat the code less like a finished artifact and more like a starting point for a conversation.

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 "multi-select dropdown" in plain HTML, CSS, and JavaScript using a Set for selection state — no dropdown/select library.

Requirements:
- A trigger element styled like a form field (role="combobox", aria-expanded, aria-haspopup="listbox") that shows a placeholder when nothing is selected, and otherwise shows one removable tag chip per selected option, wrapping onto multiple lines as needed.
- Clicking the trigger opens a dropdown panel (role="listbox", aria-multiselectable="true") positioned below it with a CSS-only open/close transition (opacity plus a small scale/translate), and clicking anywhere outside the whole control must close it.
- Inside the dropdown: a search text input that filters the visible option list by substring match against each option's label as the user types, without deselecting or losing the selected state of any option that scrolls out of view.
- Each option row must show a custom checkbox built from a plain div and a CSS ::after pseudo-element checkmark (no native checkbox, no SVG, no icon font) that fills in only when that option is selected.
- Clicking an option row must toggle its membership in a Set that tracks selected values (not an array), and that Set must be the single source of truth that both the tag-chip row and the checkbox row read from.
- Include "Select all" and "Clear all" footer buttons that operate on the same Set and re-render both the chips and the option list.
- Each tag chip needs its own remove (x) button that removes just that one value from the Set without closing the dropdown or affecting other chips.
- Show a small counter below the control reading like "3 of 8 selected" whenever at least one option is selected.

Step by step

How to Use

  1. 1
    Click the trigger to open the dropdownThe dropdown slides open with options. Click any option to select it — a chip appears in the trigger and a filled checkbox marks it in the list. Click again to deselect and remove the chip.
  2. 2
    Type in the search box to filter optionsThe inline search filters options as you type. Filtered-out options are hidden but remain selected if they were already chosen. Clear the search to see all options again.
  3. 3
    Use Select all and Clear allClick "Select all" in the footer to select every option at once. Click "Clear all" to deselect all options and reset the search input. The chip count below the trigger updates immediately.
  4. 4
    Replace the OPTIONS array with your dataUpdate the OPTIONS array at the top of the JS panel. Each object needs value (unique key), label (display text), and optionally icon (emoji or text). The dropdown renders from this array automatically.
  5. 5
    Get selected values for form submissionRead the selected Set: const values = [...selected]. For a form, set a hidden input: document.querySelector("[name=tech]").value = [...selected].join(","). On the server, split on comma to get the array of selected values.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState<Set<string>> for selected state, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Technology stack and skill selection in profile forms
Let users select multiple technologies, frameworks, or skills from a predefined list. The tag chips show the full selection at a glance. The search filter is essential for lists with 10+ options.
Filter panels for product listing and search results pages
Use for multi-select category filters, brand filters, or feature filters on e-commerce and content listing pages. Each selection adds a filter tag. The Clear all button lets users reset all filters in one click.
Team member and collaborator assignment
Assign multiple team members to a project, task, or document. The option icons can be avatar initials instead of emoji. The search filter helps find team members quickly in large organisations.
Tag management and content categorisation
Use for blog post tag selection, product category assignment, and content tagging. The tag chip display in the trigger shows the complete tag set. The inline search prevents mistyping existing tags as new ones.
Study JavaScript Set for selection state management
The multi-select uses a JavaScript Set for selected values — preventing duplicates automatically, O(1) lookup with has(), and straightforward add/delete/clear. This is a more appropriate data structure than an array for selection state.
Permission and role assignment in admin panels
Assign multiple permissions or roles to a user account. The predefined options list prevents invalid permission strings. The Select all button assigns every permission at once for superadmin accounts.

Got questions?

Frequently Asked Questions

Read the selected Set as an array: const values = [...selected]. Add a hidden input: <input type="hidden" name="technologies" id="tech-hidden">. Update it whenever selection changes: document.getElementById("tech-hidden").value = [...selected].join(","). On the server, split the comma-separated string: technologies = request.body.technologies.split(","). For native FormData submission, the hidden input value is included automatically.

Call toggle() for each option you want pre-selected before the initial renderTags() and renderOptions() calls: toggle("react"); toggle("typescript");. Or set selected = new Set(["react","typescript"]) before the initial renders. The tags will appear in the trigger and the checkboxes will be filled on page load.

In the toggle() function, add a max check: if (!selected.has(value) && selected.size >= MAX_SELECTIONS) { showError("Maximum " + MAX_SELECTIONS + " items"); return; }. Show an error message or flash the count display red. Add a visual indicator when the limit is reached: "3/3 selected" in a different colour.

Click "JSX" to download. Manage selected with useState<Set<string>>(new Set()). For toggle: setSelected(prev => { const next = new Set(prev); next.has(v) ? next.delete(v) : next.add(v); return next; }). Derive filteredOptions with useMemo([query]). The tag chips and option list render from the selected Set and OPTIONS array. Pass an onChange prop that receives [...selected] whenever the Set updates.