Web component demo
<multi-selector>
is a web component that lets a user select multiple options from a drop-down menu:
- Arbitrarily nested groups of options
- Searchable groups/values
- Keyboard navigation
- Reads options from JSON or markup
- Adaptive styling with dark mode support
- VanillaJS (no dependencies)
This page contains several examples of how to use this web component. View on GitHub.
Load from JSON
Supply the data as JSON using the src
attribute of <multi-selector>
.
Expected data structure
Each item should follow this JSON structure:
{
"label": "Display text", // optional
"value": "unique_id", // required
"children": [] // optional for leaf nodes
}
See fallacies.json for an example. When no label is provided, the value serves as the fallback display text.
Preselecting options
Add "selected": true
to any leaf node to preselect it. This property is ignored on group nodes since their selection state derives from their children.
Simplified data formats
<multi-selector>
automatically converts simpler formats:
- Flat arrays:
["option1", "option2"]
- Nested objects:
{"Group": ["item1", "item2"]}
See albums.json and countries.json for examples of alternative formats.
Code
<!-- html -->
<multi-selector
src="data/countries.json"
name="countries">
</multi-selector>
Define options in markup
Populate <multi-selector>
using standard <option>
tags. Each option uses its text content as the value unless you provide a value
attribute.
Dynamically adding options to the DOM will update the component automatically.
Note that DOM updates trigger a complete component re-render.
Grouping with optgroup
Use <optgroup>
tags to create option groups. This component supports nested <optgroup>
elements, extending beyond the standard HTML behavior.
Group labels and values default to the element's textContent
, but can be overridden with label
and value
attributes.
Add a color to the color selector:
Code
<!-- html -->
<multi-selector name="colors">
<option>Red</option>
<option>Yellow</option>
<option>Green</option>
<option>Blue</option>
</multi-selector>
Code
<!-- html -->
<multi-selector name="instruments">
<optgroup label="String instruments" value="string">
<optgroup label="Guitar" value="guitar">
<option value="acoustic">Acoustic</option>
<option value="electric">Electric</option>
</optgroup>
<option value="violin">Violin</option>
<option value="cello">Cello</option>
</optgroup>
[...]
</multi-selector>
Controls & navigation
- Select or deselect entire groups using group checkboxes
- Filter options by keyword using the search input
- Toggle the [val] button to search values only (excluding group names)
Filtering and folding
- Show only selected options: click ☑ or press Ctrl+\
- Clear all filters: click ✕ or press Ctrl+/
- Fold or unfold all groups: use +/- buttons or press Ctrl+[/Ctrl+]
Keyboard navigation
- Navigate with ↑ ↓ ← →, Home, End, and Tab
- Close the dropdown with Esc
Code
<!-- html -->
<multi-selector
src="data/fallacies.json"
name="fallacies">
</multi-selector>
Component behavior and events
Control component state with the disabled
attribute. Use placeholder
to provide helpful text when no selections are made.
Change events
The component dispatches change
events whenever selections change. The event's detail
property contains an array of selected values. Open the browser console to see the event data structure.
Code
<!-- html -->
<multi-selector
name="Select genres"
src="data/genres.json"
placeholder=
"What are your favorite media genres?"
disabled>
</multi-selector>
Form integration
<multi-selector>
works seamlessly with standard HTML forms. It participates in form submission and validation like any native form control.
Form data is submitted as a JSON string containing the selected values. For programmatic access, use the selectedValues
property which returns a live array.
Set the name
attribute to specify the form field name.
Form data
Code
<!-- html -->
<form>
<label for="Albums">Captain Beefheart</label>
<multi-selector
src="data/albums.json"
name="Albums"
placeholder=
"Select Captain Beefheart albums...">
</multi-selector>
<button type="submit">submit</button>
</form>
Styling and theming
The component adapts to its environment by inheriting text color and using transparent backgrounds. It automatically derives border colors and interactive states from the inherited color.
Dark mode support
Set mode="dark"
or let the component automatically detect system preference using prefers-color-scheme
.
Styling approaches
Customize appearance using ::part()
selectors for precise control, or CSS custom properties for simple theming:
::part(container)
- Main dropdown container::part(display)
- Selected items display area::part(options)
- Options list container--ms-*
properties - Colors, spacing, and dimensions
See the CSS reference for all available custom properties.
Code
/* css */
.styling-example multi-selector {
--ms-text-color: hsl(0, 5%, 84%);
--ms-border-color: hsl(120, 24%, 55%);
--ms-dropdown-background: hsl(120, 24%, 25%);
--ms-search-background: hsl(120, 24%, 35%);
--ms-hover: hsl(120, 24%, 30%);
--ms-search-placeholder-text-color: hsl(0, 5%, 68%);
width: 300px;
margin: 0;
}
.styling-example multi-selector::part(container) {
border-width: 2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.styling-example multi-selector::part(dropdown) {
background: var(--ms-dropdown-background);
}
.styling-example multi-selector::part(options) {
border-radius: 8px;
border: 1px solid hsl(120, 24%, 40%);
}
Empty state and dynamic data
Components without initial data display an empty state message and remain disabled until data is provided.
Set or update data programmatically using the data
property. Assign an array of objects or set to an empty array to clear all options.
Code
<!-- html -->
<button id="add-data">Add data</button>
<button id="clear-data" disabled>Clear data</button>
<multi-selector name="fruits"></multi-selector>
// javascript
const fruitSelector = document.querySelector("multi-selector[name='fruits']")
const addDataButton = document.getElementById("add-data")
const clearDataButton = document.getElementById("clear-data")
const fruits = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
]
addDataButton.addEventListener("click", () => {
fruitSelector.data = fruits
addDataButton.disabled = true
clearDataButton.disabled = false
})
clearDataButton.addEventListener("click", () => {
fruitSelector.data = []
clearDataButton.disabled = true
addDataButton.disabled = false
})
CSS Reference
Complete reference for styling <multi-selector>
components.
Custom Properties
Use these CSS custom properties for theming:
Layout & Spacing
--ms-height
- Component height (default:calc(2rem + var(--ms-padding-block))
)--ms-max-height
- Maximum dropdown height (default:60vh
)--ms-padding-block
- Vertical padding (default:.25em
)--ms-padding-inline
- Horizontal padding (default:1em
)--ms-border-radius
- Border radius (default:5px
)
Base Colors
--ms-text-color
- Main text color (default:inherit
)--ms-border-color
- Border color (default:currentColor
)--ms-checkbox-color
- Checkbox fill color (default:currentColor
)
Derived Colors
--ms-text-color-disabled
- Disabled text color--ms-border-color-disabled
- Disabled border color
Backgrounds
--ms-dropdown-background
- Dropdown container background--ms-search-background
- Search input background--ms-hover
- Hover state background
Button States
--ms-button-background
- Button background--ms-button-background-hover
- Button hover state--ms-button-background-active
- Button active state
Search Input
--ms-search-text-color
- Search input text color--ms-search-placeholder-text-color
- Search placeholder color
Shadow Parts
Use ::part()
selectors for structural styling:
::part(container)
- Main dropdown container (<details>
)::part(display)
- Selected items display area::part(controls)
- Control button panel::part(control-button)
- Individual control buttons::part(dropdown)
- Dropdown content area::part(filter)
- Filter/search section::part(search)
- Search input field::part(options)
- Options container
Theme Modes
The component automatically adapts to dark/light themes:
mode="light"
- Force light thememode="dark"
- Force dark theme- No mode attribute - Automatically follows
prefers-color-scheme
Styling Priority
When both custom properties and ::part()
rules target the same property, ::part()
rules win due to higher CSS specificity. Use ::part()
for precise control and custom properties for theming.