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.
| Name | Type | Default | Notes |
|---|---|---|---|
| options | SelectOption[] | null | — | Nullable — renders an empty list rather than throwing. group and description are strings; icon is a ReactNode. |
| valuetwo-way | string | — | |
| defaultValue | string | '' | |
| onValueChangeevent | (value: string) => void | — | |
| placeholder | string | — | |
| hint | string | — | Standing help under the control, always shown. |
| error | string | — | What 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. |
| emptyLabel | string | 'No results' | |
| opentwo-way | boolean | — | Uncontrolled via defaultOpen. |
| onOpenChangeevent | (open: boolean) => void | — | |
| renderOption | (option, state) => ReactNode | — | Replaces 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 }. |
| Name | Type | Default | Notes |
|---|---|---|---|
| options | SelectOption[] | null | — | |
| modelValuetwo-way | string | — | |
| update:modelValueevent | (value: string) => void | — | |
| placeholder | string | — | |
| hint | string | — | Standing help under the control, always shown. |
| error | string | — | What 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. |
| emptyLabel | string | 'No results' | |
| opentwo-way | boolean | — | |
| optionslot | slot({ 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. |
| Name | Type | Default | Notes |
|---|---|---|---|
| options | SelectOption[] | null | — | |
| valuetwo-way | string | — | Bindable, which is why there is no defaultValue: one prop covers both modes where React needs a pair. |
| onchangeevent | (value: string) => void | — | |
| placeholder | string | — | |
| hint | string | — | Standing help under the control, always shown. |
| error | string | — | What 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. |
| emptyLabel | string | 'No results' | |
| opentwo-way | boolean | — | |
| optionslot | Snippet<[SelectOption, AuraOptionRenderState]> | — |
| Name | Type | Default | Notes |
|---|---|---|---|
| options | SelectOption[] | null | — | |
| valuetwo-way | string | — | |
| valueChangeevent | (value: string) => void | — | |
| placeholder | string | — | |
| hint | string | — | Standing help under the control, always shown. |
| error | string | — | What 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. |
| emptyLabel | string | 'No results' | |
| opentwo-way | boolean | — | |
| optionTemplate | TemplateRef | — | A 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.
| Class | Role | Notes |
|---|---|---|
| .aura-combobox | base | Required. A modifier does nothing without it. |
| .aura-combobox__value | element | |
| .aura-combobox__chevron | element | |
| .aura-popup | base | Required. A modifier does nothing without it. |
| .aura-listbox | base | Required. A modifier does nothing without it. |
| .aura-listbox__option | element |
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)