Primitives
15

MultiSelect

Pick several. Each choice becomes a chip in the field, and the field is a text input you filter the list from — so choosing and searching happen in one place instead of two. role="combobox" sits on that input rather than on the box around it: the thing announced as the list's owner has to be the thing the keyboard is in. The chips are Chip, at sm. One thing the pattern does not give you, in any of the four: a screen reader on the input does not read the chips back. Each chip's dismiss button carries its own name ("Remove Apple"), which is the part that is reachable, and APG's own multiselect has the same shape — recorded here rather than patched with an aria-describedby that would go stale.

plain markup — no library on the page
rendered by aura-react
rendered by aura-vue
rendered by aura-svelte
built from aura-angular's own class helpers
Apple
Apple
Apple
Apple
Apple

Code

index.html
<div class="aura-field-group">
  <label class="aura-label" id="m-label" for="m">Tags</label>
  <div class="aura-combobox aura-combobox--multi" data-state="closed">
    <span class="aura-chip aura-chip--sm">Apple
      <button type="button" class="aura-chip__remove" aria-label="Remove Apple">
        <svg viewBox="0 0 8 8" aria-hidden="true"><path d="M1 1l6 6M7 1l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>
      </button>
    </span>
    <input id="m" type="text" class="aura-combobox__input" role="combobox" aria-expanded="false"
           aria-controls="m-listbox" aria-autocomplete="list" aria-labelledby="m-label" autocomplete="off" value="" />
    <span class="aura-combobox__chevron" aria-hidden="true"></span>
  </div>
</div>
Demo.tsx
import { MultiSelect } from '@ilomee/aura-react';

const OPTIONS = [
  { value: 'apple', label: 'Apple' },
  { value: 'pear', label: 'Pear' },
  { value: 'cherry', label: 'Cherry' },
];

/**
 * Mirrors the `multi-select` html snippet, which is what every stack is diffed against — so the
 * label and the chip are its label and its chip, not a nicer pair.
 *
 * Closed, and no layout wrapper: the open list is portalled to `document.body`, where nothing
 * reading the preview panel can see it, and a wrapper would be a node the snippet does not have.
 */
export default function Demo() {
  return <MultiSelect label="Tags" options={OPTIONS} defaultValue={['apple']} />;
}
Demo.vue
import { MultiSelect } from '@ilomee/aura-vue';
Demo.svelte
import { MultiSelect } from '@ilomee/aura-svelte';

Used as aura-multi-select.

demo.component.ts
import { AuraMultiSelect } from '@ilomee/aura-angular';

Props

Plain markup has no props — the classes below are the whole API. The four packages do nothing more than set these same classes for you.
NameTypeDefaultNotes
optionsSelectOption[] | null
valuetwo-waystring[]
defaultValuestring[][]
onValueChangeevent(value: string[]) => void
hintstringStanding help under the control, always shown.
errorstringWhat is wrong right now. Sets aria-invalid on the control and adds the message to its aria-describedby, after the hint. No live region: role="alert" announces on mount, so a server-rendered form with three errors would interrupt three times before the reader reached the first field.
searchablebooleantrueOff makes the chip row the whole control — the input stays, read-only, so the keyboard still works.
maxnumberAt the cap the unchosen options go aria-disabled rather than disappearing — a list that shrinks as you pick is a list you cannot see what you missed in.
removeLabelstring'Remove {label}'{label} is replaced with the option's label — a template, because the noun sits in a different place in each language.
renderOption(option, state) => ReactNode
NameTypeDefaultNotes
modelValuetwo-waystring[]
update:modelValueevent(value: string[]) => void
hintstringStanding help under the control, always shown.
errorstringWhat is wrong right now. Sets aria-invalid on the control and adds the message to its aria-describedby, after the hint. No live region: role="alert" announces on mount, so a server-rendered form with three errors would interrupt three times before the reader reached the first field.
searchablebooleantrue
maxnumber
optionslotslot({ option, index, active, selected, disabled })
NameTypeDefaultNotes
valuetwo-waystring[]
onchangeevent(value: string[]) => void
hintstringStanding help under the control, always shown.
errorstringWhat is wrong right now. Sets aria-invalid on the control and adds the message to its aria-describedby, after the hint. No live region: role="alert" announces on mount, so a server-rendered form with three errors would interrupt three times before the reader reached the first field.
searchablebooleantrue
maxnumber
optionslotSnippet<[SelectOption, AuraOptionRenderState]>
NameTypeDefaultNotes
valuetwo-waystring[]
valueChangeevent(value: string[]) => void
hintstringStanding help under the control, always shown.
errorstringWhat is wrong right now. Sets aria-invalid on the control and adds the message to its aria-describedby, after the hint. No live region: role="alert" announces on mount, so a server-rendered form with three errors would interrupt three times before the reader reached the first field.
searchablebooleantrue
maxnumber
optionTemplateTemplateRef

Emitted classes

Every framework emits the same classes — that is what makes the four libraries look identical. You can use them directly, without any component library.

ClassRoleNotes
.aura-comboboxbaseRequired. A modifier does nothing without it.
.aura-combobox--multimodifier
.aura-combobox__inputelement
.aura-chipbaseRequired. A modifier does nothing without it.
.aura-chip--smmodifier
.aura-listboxbaseRequired. A modifier does nothing without it.

Tokens and rules

components.css
chip row
calc((var(--control-h) - 26px) / 2 - 1px) — one row lands on --control-h, a second grows the box
chips
.aura-chip--sm — 26px, the "editable tag lists" variant
panel layer
var(--z-dropdown)