More Forms Snippets
Multi-Select Dropdown — Free HTML CSS JS Snippet
Multi-Select Dropdown · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Multi-Select Dropdown — Tag Chips, Inline Search, Custom Checkboxes & Select All

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:
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
- 1Click 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.
- 2Type 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.
- 3Use 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.
- 4Replace 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.
- 5Get 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.
- 6Export 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
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.