MultiSelect
MoleculeChip-based multi-select popover with searchable filter, keyboard navigation, and disabled-option support.
React
TypeScript
Pick the technologies you know.
<%- include('modules/ui/MultiSelect', {
label: 'Skills',
options: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'typescript', label: 'TypeScript' },
],
value: ['react', 'typescript'],
hint: 'Pick the technologies you know.'
}) %>
React
<%- include('modules/ui/MultiSelect', {
label: 'Skills',
options: SKILLS,
value: ['react'],
searchable: true
}) %>
Choose roles…
<%- include('modules/ui/MultiSelect', {
label: 'Roles',
options: ROLES,
placeholder: 'Choose roles…'
}) %>
Select…
Please pick at least one role.
<%- include('modules/ui/MultiSelect', {
label: 'Roles',
options: ROLES,
error: 'Please pick at least one role.'
}) %>
React
TypeScript
<%- include('modules/ui/MultiSelect', {
label: 'Skills',
options: SKILLS,
value: ['react', 'typescript'],
disabled: true
}) %>
<%
// MultiSelect (lives in the ComboBox folder so it can share _chip/_listbox
// partials and the filter/async/keyboard helpers). Mirrors NextJS
// modules/ui/MultiSelect.tsx M1.
var _id = locals.id || 'multiselect-' + Math.random().toString(36).substr(2, 9);
var _label = locals.label || '';
var _options = locals.options || [];
var _value = locals.value || [];
var _placeholder = locals.placeholder || 'Select…';
var _hint = locals.hint || '';
var _error = locals.error || '';
var _dis = !!locals.disabled;
var _searchable = !!locals.searchable;
var _className = locals.className || '';
var labelId = _id + '-label';
var hintId = _hint ? (_id + '-hint') : '';
var errorId = _error ? (_id + '-error') : '';
var describedBy = [hintId, errorId].filter(Boolean).join(' ');
var listboxId = _id + '-listbox';
function isSelected(v) {
for (var i = 0; i < _value.length; i++) { if (_value[i] === v) return true; }
return false;
}
var shellStateClass = _error
? 'border-error ring-1 ring-error bg-error-subtle'
: 'border-border bg-surface-base';
var shellDisabledClass = _dis ? ' opacity-50 cursor-not-allowed bg-surface-sunken' : '';
%>
aria-describedby="<%= describedBy %>"<% } %>
aria-disabled="<%= _dis ? 'true' : 'false' %>"
id="<%= _id %>"
data-multiselect-shell
class="min-h-[2.5rem] w-full rounded-md border px-3 py-1.5 text-sm transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus flex flex-wrap gap-1 items-center <%= shellStateClass %><%= shellDisabledClass %>"
>
<% _value.forEach(function(v) {
var opt = null;
for (var i = 0; i < _options.length; i++) { if (_options[i].value === v) { opt = _options[i]; break; } }
var chipValue = v;
var chipLabel = opt ? opt.label : v;
var chipIcon = (opt && opt.icon) ? opt.icon : '';
%>
<%- include('./partials/_chip', { chipValue: chipValue, chipLabel: chipLabel, chipIcon: chipIcon }) %>
<% }); %>
<% if (_hint && !_error) { %><%= _hint %>
<% } %>
<% if (_error) { %><%= _error %>
<% } %>