Primitives
14

Select

A select backed by a real listbox: grouping, descriptions, and options you render yourself — none of which a native <select> can carry, which is why this is the one the library ships. The trigger is a <button role="combobox">, so focus never leaves it: the cursor through the list is virtual, and there is nothing to trap and nothing to restore.

plain markup — no library on the page
rendered by aura-react
rendered by aura-react
rendered by aura-react
rendered by aura-react
  • Apple
  • CherryIn season

Code

index.html
<div class="aura-field-group">
  <label class="aura-label" id="s-label" for="s">Fruit</label>
  <button id="s" type="button" class="aura-combobox" role="combobox" aria-haspopup="listbox"
          aria-expanded="true" aria-controls="s-listbox" aria-labelledby="s-label s"
          aria-activedescendant="s-opt-0" data-state="open">
    <span class="aura-combobox__value">Apple</span>
    <span class="aura-combobox__chevron" aria-hidden="true"></span>
  </button>
</div>
<!-- No data-side here. That attribute is what place() writes, and .aura-popup[data-side] is
     what turns the panel into position: fixed — statically there is nothing to anchor to, so a
     fixed panel would just collapse into the corner of the window. Left off, the panel stays in
     flow and shows what the list looks like; in a real Select, place() supplies the attribute
     and the insets in the same frame that mounts it. -->
<div class="aura-popup" data-state="open">
  <ul id="s-listbox" class="aura-listbox" role="listbox" aria-labelledby="s-label">
    <li id="s-opt-0" class="aura-listbox__option" role="option" aria-selected="true" data-active="true">
      <span class="aura-listbox__label">Apple</span>
    </li>
    <li id="s-opt-1" class="aura-listbox__option" role="option" aria-selected="false" data-active="false">
      <span class="aura-listbox__label">Cherry<span class="aura-listbox__description">In season</span></span>
    </li>
  </ul>
</div>
Demo.tsx
import { Select, Stack } from '@ilomee/aura-react';

const OPTIONS = [
  { value: 'apple', label: 'Apple', group: 'Pome' },
  { value: 'pear', label: 'Pear', group: 'Pome' },
  { value: 'cherry', label: 'Cherry', description: 'In season', group: 'Stone' },
  { value: 'durian', label: 'Durian', description: 'Out of stock', group: 'Stone', disabled: true },
];

/**
 * `minWidth`, as the Field demo does it: the preview panel is shrink-to-fit, and a combobox
 * sized to its own placeholder would resize the moment you picked something longer. In a form
 * the control's `inline-size: 100%` handles this; a preview has no form to fill.
 */
export default function Demo() {
  return (
    <Stack style={{ minWidth: 260 }}>
      <Select label="Fruit" placeholder="Pick one" options={OPTIONS} />
    </Stack>
  );
}
Demo.vue
<script setup lang="ts">
import { Select } from '@ilomee/aura-vue';
const fruit = ref('');
</script>

<template>
  <Select v-model="fruit" label="Fruit" placeholder="Pick one" :options="options" />
</template>
Demo.svelte
<script lang="ts">
  import { Select } from '@ilomee/aura-svelte';
  let fruit = $state('');
</script>

<Select bind:value={fruit} label="Fruit" placeholder="Pick one" {options} />

Used as aura-select.

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

@Component({
  standalone: true,
  imports: [AuraSelect],
  template: `<aura-select label="Fruit" placeholder="Pick one" [options]="options" [(value)]="fruit" />`,
})
export class DemoComponent {
  fruit = '';
}

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[] | nullNullable — renders an empty list rather than throwing. group and description are strings; icon is a ReactNode.
valuetwo-waystring
defaultValuestring''
onValueChangeevent(value: string) => void
placeholderstring
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.
emptyLabelstring'No results'
opentwo-waybooleanUncontrolled via defaultOpen.
onOpenChangeevent(open: boolean) => void
renderOption(option, state) => ReactNodeReplaces the option row contents — never the <li>. Its role, id, aria-selected and class stay Aura's, so the hook cannot break accessibility. state is { index, active, selected, disabled }.
NameTypeDefaultNotes
optionsSelectOption[] | null
modelValuetwo-waystring
update:modelValueevent(value: string) => void
placeholderstring
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.
emptyLabelstring'No results'
opentwo-wayboolean
optionslotslot({ option, index, active, selected, disabled })Vue's escape hatch is a scoped slot rather than React's renderOption prop — same payload, each framework's own idiom. It fills the option row; the <li> stays Aura's.
NameTypeDefaultNotes
optionsSelectOption[] | null
valuetwo-waystringBindable, which is why there is no defaultValue: one prop covers both modes where React needs a pair.
onchangeevent(value: string) => void
placeholderstring
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.
emptyLabelstring'No results'
opentwo-wayboolean
optionslotSnippet<[SelectOption, AuraOptionRenderState]>
NameTypeDefaultNotes
optionsSelectOption[] | null
valuetwo-waystring
valueChangeevent(value: string) => void
placeholderstring
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.
emptyLabelstring'No results'
opentwo-wayboolean
optionTemplateTemplateRefA bound TemplateRef, not a <ng-content> query — the idiom AuraDataTable already uses, so the template is declared where its scope is. Context: let-option let-state="state". It fills the option row; the <li> stays ours.

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__valueelement
.aura-combobox__chevronelement
.aura-popupbaseRequired. A modifier does nothing without it.
.aura-listboxbaseRequired. A modifier does nothing without it.
.aura-listbox__optionelement

Tokens and rules

components.css
frame
var(--field-border) · var(--field-radius) — min-block-size, not height
panel layer
var(--z-dropdown) — under a modal, on purpose
gap to anchor
var(--popover-offset)
panel
var(--shadow-3) · max-block-size: min(320px, 60vh)