Custom objects, tuple arrays, member/getter callbacks, async search, form integration (json/csv/array), public API methods, and cascading selects.
🌐 client — runs in the browser (element JS API) 🖥️ server — LiveView round-trip
value_member="userId" · display_value_member="fullName" · subtitle_member="email"
Use your own data structure! Specify which properties to use for value, display, and other fields using value_member
and display_value_member.
Just pass tuples to options={@languages} — no value_member/display_value_member needed
Auto-detected! Simply pass an array of [key, value]
tuples. Perfect for simple key-value pairs without creating objects.
Use callbacks when you need complex logic to extract values. Callbacks take precedence over members.
getValueCallback, getDisplayValueCallback, getSubtitleCallback
and getDisabledCallback
can't be serialized as HTML attributes — they're assigned to the element by id in the inline script below.
name="skills" + value_format="json" writes a hidden input the form submits
Seamless HTML form integration! Hidden inputs are automatically created and updated. Choose from 3 formats: json, csv, or array.
value_format="csv" submits a single comma-separated hidden input
Use CSV format for traditional comma-separated values. Great for legacy systems.
value_format="array" emits one hidden input per value as tags[]
Use array format to create multiple hidden inputs. Standard HTML array handling with name[].
(submit the form to see captured params)
multiple={false} switches single-select mode (returns a scalar); omit it for multi-select
New properties and methods for easier value access:
getValue() - Returns single value or array depending on modeselectedValue - Get the selected value(s)selectedItem - Get the first selected item objectgetSelected() - Get all selected item objectsChange the selection, then click a method above.
Change the selection, then click a method above.
Drive the dropdown open state from your own code — wizards, "open on step", or custom trigger buttons. Calling open() from a button's own click handler works directly: the component ignores that opening click so it won't immediately re-close.
open() - Open the dropdownclose() - Close the dropdowntoggle() - Toggle the dropdown open/closedisOpen - Boolean property; reading returns the state, assigning true/false opens/closes itfalse
const multiselect = document.querySelector('web-multiselect');
multiselect.open(); // Open the dropdown
multiselect.close(); // Close the dropdown
multiselect.toggle(); // Toggle open/closed
multiselect.isOpen; // → true / false
multiselect.isOpen = true; // open by assignment
multiselect.isOpen = false; // close by assignment
Read the current search box text with searchText, set it programmatically with search(term) — it applies exactly as if the user typed (runs beforeSearchCallback / minSearchLength / async searchCallback) and does not open the dropdown — and reset it with clearSearch(). There's no separate "filter" method: what the query does is governed by searchMode — set el.searchMode = 'filter' (narrow the list) or 'navigate' (keep all options, jump focus to the match) before searching. Handy for deep-links ("open with a query pre-filled"), syncing an external field, or restoring saved state.
searchText - read the current query (empty string when nothing is typed)search(term) - set the query and apply it (does not open the dropdown)searchMode - 'filter' | 'navigate'; governs what the query doesclearSearch() - clear the query and restore the full list
Try an in each mode: filter narrows to Banana / Mango / Orange; navigate keeps every fruit visible and just focuses the first match.
""
const el = document.querySelector('web-multiselect');
el.searchMode = 'filter'; // or 'navigate' — governs how the query behaves
el.search('an'); // apply (as if typed); flush() applies the mode first
el.open(); // …call open() to see the effect
el.searchText; // → "an"
el.clearSearch(); // → ""
defer attribute holds the first render · release with el.ready()
or by removing the attribute
With defer={true} the element builds nothing on upgrade —
it only reserves space. Wire options, callbacks (customStylesCallback) and
listeners first, then call el.ready() to build once,
flash-free. This closes the classic upgrade-then-restyle flash where a component paints
with its default styles for a beat before a post-upgrade callback (or a framework's
shared-stylesheet adoption) restyles it — the styled result appears in one shot.
defer — boolean attribute; present = hold the first renderready() — release the gate and build once (latched — never re-closes)defer attribute also releases it — this is how the wrapper's
shared-styles registry does it, server-driven, with no JS hook
isReady — reflected as the is-ready attribute (CSS hook
:host([defer]:not([is-ready])) reserves space while held)
ready — event dispatched once, right after the first buildheld — waiting for data…
:shadow_styles is configured the wrapper adds defer to every
<web-multiselect> automatically, and its registry adopts the shared sheet
then removes the attribute — themed badges paint flash-free with no per-instance JS. See the
💧 Elixir Only page. Reach for defer={true} + your own
hook/ready() (as below) only when you need to hold the build for
async wiring.
// <.web_multiselect id="defer-demo" defer={true} value={["js", "ts"]} />
// `defer` holds the build, so the element paints NOTHING until ready().
const el = document.querySelector('#defer-demo');
// Wire everything WHILE HELD — nothing renders yet:
el.customStylesCallback = () => `.ms__badge { background: #6d28d9; color: #fff; }`;
el.addEventListener('ready', () => console.log('built flash-free', el.isReady));
const langs = await loadLanguages(); // e.g. a 2 s fetch
el.options = langs; // assign options…
el.ready(); // …then release: builds ONCE, styled, in one shot
// In LiveView you'd wire this from a phx-hook's mounted() and call el.ready() after your
// async options land. For the shared-theme case you need NONE of this — configure
// :shadow_styles and the wrapper defers + releases every select for you (see Elixir Only).
min_search_length={2} · search_debounce={300} · icon_member="flag"
Load options dynamically as users type. Perfect for large datasets, API searches, or real-time filtering.
searchCallback
receives an AbortSignal
as its 2nd argument. Forward it to fetch
so a superseded keystroke cancels the in-flight request. Pair with search_debounce
to also coalesce keystroke bursts. These callbacks are assigned to each element by id in the inline script below.
Crank the delay up, then type quickly — each keystroke fires a request and cancels the previous in-flight one (watch the log below).
search-debounce)
The API call fires only after you pause typing for the debounce window — so a fast burst of keystrokes collapses into a single request. Set it to 0
to fire on every keystroke.
Create dependent dropdowns that automatically update! Setting element.options = newArray
triggers automatic re-rendering. Perfect for Organization → Business Unit → Department flows. This card mirrors upstream's pure-JS reactive cascade; for the server-driven (LiveView) variant see EO03 on the Elixir Only page.
{
"organization": null,
"businessUnit": null,
"department": null
}
enable_badge_tooltips={true} · badge_tooltip_placement="top"
Show informative tooltips when hovering over badges and remove buttons. Perfect for truncated text or additional context.
getBadgeTooltipCallback
(assigned in the inline script) for rich content. Remove-button tooltips show "Remove [item name]"; placement is configurable (top, bottom, left, right).
Show different text in badges vs dropdown options. This example shows full details in dropdown ("John Doe - Senior Developer") with subtitle showing email, but just first names in badges ("John") for maximum space efficiency!
getDisplayValueCallback
drives the dropdown text while getBadgeDisplayCallback
drives the compact badge text — both assigned in the inline script. It falls back to the standard display value when getBadgeDisplayCallback
isn't provided.
DOM events (no attribute): el.addEventListener("select" | "deselect" | "change", …) — see the inline <script>
Listen to select, deselect, and change events
on* property handlers and the
beforeSelect/beforeDeselect interceptors?
See Events, Handlers & Interceptors.
Assign .options
and call setSelected(...)
on the very next line — no await. The property write coalesces onto a microtask, but the
imperative call flushes it, so the pre-set selection renders immediately. Shown for the
partial
and compact
badge modes (the compact composite badge is what previously mis-rendered when the two calls raced).
const el = document.querySelector('#api06-compact');
el.options = languages; // reactive write (coalesces on a microtask)
el.setSelected(['js', 'ts', 'py']); // imperative call flushes it synchronously
(pick an option anywhere on this page)