A select built on the browser's own <select> element, styled to match the rest of the form components. Reach for it when you want the platform's picker — the native dropdown on mobile, the OS list box on desktop — instead of the custom listbox that Select renders.
import { NativeSelect } from 'frontile';
Pass a collection to @items and read the selection back from @onSelectionChange. Strings and numbers are used as both the key and the label.
Selected: Cheetah
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { NativeSelect } from 'frontile';
const animals = ['Cheetah', 'Crocodile', 'Elephant'];
export default class NativeSelectUsage extends Component {
@tracked selectedKey: string | null = 'Cheetah';
onSelectionChange = (key: string | null) => {
this.selectedKey = key;
};
<template>
<NativeSelect
@label='Favorite animal'
@items={{animals}}
@selectedKey={{this.selectedKey}}
@onSelectionChange={{this.onSelectionChange}}
/>
<p class='mt-4 text-sm text-neutral'>Selected: {{this.selectedKey}}</p>
</template>
}
The component is controlled. @selectedKey is what the user sees selected, so it has to be updated from @onSelectionChange — without that, the browser's change is reverted on the next render.
Objects work too. The key comes from key or id, and the label from label, value, name or title. Anything else needs the :item block, which yields the raw item along with an Item component to render it with.
Selected:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { NativeSelect } from 'frontile';
const countries = [
{ code: 'br', country: 'Brazil' },
{ code: 'ca', country: 'Canada' },
{ code: 'jp', country: 'Japan' }
];
export default class NativeSelectItemBlock extends Component {
@tracked selectedKey: string | null = null;
onSelectionChange = (key: string | null) => {
this.selectedKey = key;
};
<template>
<NativeSelect
@label='Country'
@items={{countries}}
@selectedKey={{this.selectedKey}}
@onSelectionChange={{this.onSelectionChange}}
>
<:item as |o|>
<o.Item @key={{o.item.code}}>{{o.item.country}}</o.Item>
</:item>
</NativeSelect>
<p class='mt-4 text-sm text-neutral'>Selected: {{this.selectedKey}}</p>
</template>
}
When the options are a fixed, hand-written list, drop @items entirely and use the default block, which yields the same Item component.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { array } from '@ember/helper';
import { NativeSelect } from 'frontile';
export default class NativeSelectStaticItems extends Component {
@tracked selectedKey: string | null = 'standard';
onSelectionChange = (key: string | null) => {
this.selectedKey = key;
};
<template>
<NativeSelect
@label='Shipping'
@selectedKey={{this.selectedKey}}
@disabledKeys={{(array 'overnight')}}
@onSelectionChange={{this.onSelectionChange}}
as |l|
>
<l.Item @key='standard'>Standard — 5 business days</l.Item>
<l.Item @key='express'>Express — 2 business days</l.Item>
<l.Item @key='overnight'>Overnight — unavailable</l.Item>
</NativeSelect>
</template>
}
@disabledKeys renders those options as disabled: still listed, still announced, not selectable.
A native <select> always has something selected, so an initial "nothing chosen" state has to be a real option. @allowEmpty adds one, labeled with @placeholder, and picking it reports null.
Selected: none
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { NativeSelect } from 'frontile';
const roles = ['Owner', 'Editor', 'Viewer'];
export default class NativeSelectAllowEmpty extends Component {
@tracked selectedKey: string | null = null;
onSelectionChange = (key: string | null) => {
this.selectedKey = key;
};
<template>
<NativeSelect
@label='Role'
@items={{roles}}
@allowEmpty={{true}}
@placeholder='Select a role'
@selectedKey={{this.selectedKey}}
@onSelectionChange={{this.onSelectionChange}}
/>
<p class='mt-4 text-sm text-neutral'>Selected:
{{if this.selectedKey this.selectedKey 'none'}}</p>
</template>
}
@selectionMode='multiple' renders the native multi-select list box. The state arguments change with it: use @selectedKeys instead of @selectedKey, and @onSelectionChange receives an array. Mixing them logs a warning.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { NativeSelect } from 'frontile';
const languages = ['Elixir', 'Go', 'Rust', 'TypeScript'];
export default class NativeSelectMultiple extends Component {
@tracked selectedKeys: string[] = ['Rust'];
onSelectionChange = (keys: string[]) => {
this.selectedKeys = keys;
};
<template>
<NativeSelect
@label='Languages'
@description='Hold Cmd or Ctrl to select more than one.'
@selectionMode='multiple'
@items={{languages}}
@selectedKeys={{this.selectedKeys}}
@onSelectionChange={{this.onSelectionChange}}
/>
</template>
}
@size accepts sm, md (the default) and lg, matching the other form controls.
import { array, concat } from '@ember/helper';
import { NativeSelect } from 'frontile';
const plans = ['Basic', 'Pro'];
<template>
<div class='flex flex-col gap-4'>
{{#each (array 'sm' 'md' 'lg') as |size|}}
<NativeSelect
@label={{concat 'Plan (' size ')'}}
@size={{size}}
@items={{plans}}
/>
{{/each}}
</div>
</template>
NativeSelect is built on the same FormControl as the other form components, so @label, @description, @isRequired, @errors and @isInvalid behave identically. Errors are wired to the control through aria-describedby and set aria-invalid.
import { array } from '@ember/helper';
import { NativeSelect } from 'frontile';
const environments = ['Development', 'Staging', 'Production'];
<template>
<div class='flex flex-col gap-4'>
<NativeSelect
@label='Environment'
@description='Where the build will be deployed.'
@isRequired={{true}}
@items={{environments}}
/>
<NativeSelect
@label='Environment'
@errors={{(array 'Select an environment to continue.')}}
@allowEmpty={{true}}
@placeholder='Select an environment'
@items={{environments}}
/>
</div>
</template>
The :startContent and :endContent named blocks place content inside the control. The chevron the component draws stays at the end, after whatever :endContent yields.
By default pointer events pass through end content to the select and are captured by start content; @startContentPointerEvents and @endContentPointerEvents flip that when the content itself needs to be clickable.
import { NativeSelect } from 'frontile';
import { SearchIcon } from 'site/components/icons';
const teams = ['Design', 'Engineering', 'Support'];
<template>
<NativeSelect @label='Team' @items={{teams}}>
<:startContent>
<SearchIcon class='size-icon-md text-neutral' />
</:startContent>
</NativeSelect>
</template>
NativeSelect renders a real <select>, so keyboard behavior, type-ahead and the mobile picker are the browser's own and match whatever the user's platform does elsewhere. That is the main reason to choose it over Select.
| Key | Behavior |
|---|---|
| Space / Alt + ↓ | Open the picker (browser dependent) |
| ↑ / ↓ | Move through options |
| Home / End | First / last option |
| Printable characters | Jump to the option starting with those characters |
| Enter / Esc | Commit / dismiss the open picker |
| Cmd / Ctrl + click, Shift + ↑/↓ | Extend the selection in multiple mode |
The component's own responsibilities:
@label renders a <label> bound to the select by id. Always provide it — there is no visual affordance that substitutes for it, and an unlabeled select is announced only by its current value.@description and @errors are referenced through aria-describedby, and an invalid control gets aria-invalid="true".@disabledKeys sets the disabled attribute on the corresponding <option>, which screen readers announce as unavailable.For multiple selection, say so in @description as the demo above does: nothing in the native list box announces that Cmd or Ctrl extends the selection, and it is the most commonly missed interaction in the component.
Element: <span class="hljs-title class_">Array</span>
| Name | Type | Default | Description |
|---|---|---|---|
allowEmpty
|
boolean
|
false
|
Renders an extra empty option, letting the user clear the selection. Label it with the placeholder argument. |
classes
|
SlotsToClasses<'base' | 'input' | 'innerContainer' | 'startContent' | 'endContent' | 'icon'>
|
- | Per-slot class overrides: base, innerContainer, input, startContent, endContent and icon. |
description
|
string
|
- |
Help text rendered between the label and the control, and referenced by the
ids describedBy returns.
|
disabledKeys
|
Array
|
- | Keys of the options that cannot be selected. Disabled options are still rendered and announced, they just cannot be picked. |
endContentPointerEvents
|
enum
|
'none'
|
Controls pointer-events property of endContent.
Defauled to none to pass the click event to the input. If your content
needs to capture events, consider adding pointer-events-auto class to that
element only.
|
errors
|
enum
|
- |
Validation messages for the field. A non-empty value also marks the control
invalid, and an array is joined with ; when displayed.
|
id
|
string
|
- | Id applied to the select element and referenced by its label. One is generated when omitted. |
isDisabled
|
boolean
|
false
|
Whether the field is disabled. FormControl passes this through for styling;
the control it wraps is responsible for the disabled attribute.
|
isInvalid
|
boolean
|
false
|
Marks the control invalid without supplying messages, for validation that is reported elsewhere. |
isRequired
|
boolean
|
false
|
Whether the field is required. Adds an asterisk to the label; it does not
set the required attribute on the control itself.
|
items
|
Array
|
- | Collection used to render the options. Strings and numbers become both the key and the label; objects use key or id for the key and label, value, name or title for the label. Use the :item block when your data does not follow those conventions. |
label
|
string
|
- |
The label text rendered above the control and associated with it via for.
Use the :label block instead when the label needs markup.
|
name
|
string
|
- | Name of the select element, used when the surrounding form is submitted. |
onAction
|
function
|
- | Called with the key of an option whenever it is acted upon, including when the same option is picked again. Use onSelectionChange to track state. |
onItemsChange
|
function
|
- | |
onSelectionChange
|
function
|
- | Called with the newly selected key, or null when the selection is cleared through the allowEmpty option. Called with every selected key each time the selection changes. |
placeholder
|
string
|
- |
Placeholder text used when allowEmpty is set to true.
|
selectedKey
|
string
|
- | The selected key in single selection mode. The component is controlled: update this from onSelectionChange. Not available in multiple selection mode; use selectedKeys. |
selectedKeys
|
Array
|
- | Not available in single selection mode; use selectedKey. The selected keys in multiple selection mode. The component is controlled: update this from onSelectionChange. |
selectionMode
|
enum
|
'single'
'single'
|
Whether one option or several can be selected. Multiple renders the native multi-select list box and switches to selectedKeys. |
size
|
enum
|
'md'
|
The size of the control. |
startContentPointerEvents
|
enum
|
'auto'
|
Controls pointer-events property of startContent.
If you want to pass the click event to the input, set it to none.
|
| Name | Type | Default | Description |
|---|---|---|---|
item
*
|
Array
|
- | |
default
*
|
Array
|
- | |
startContent
*
|
Array
|
- | |
endContent
*
|
Array
|
- |
Element: <span class="hljs-title class_">HTMLOptionElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
key
*
|
string
|
- | Value of the rendered option, and the key reported by onSelectionChange, selectedKey or selectedKeys, and disabledKeys. |
manager
*
|
ListManager
|
- | The ListManager that tracks selection for the parent select. It is bound for you on the yielded Item component. |
item
|
unknown
|
- |
The entry of @items this option renders, remembered on the registered
list item so a selection can hand it back. Bound for you on the Item
yielded from the :item block; block-form options have no such entry.
|
textValue
|
string
|
- | Text representation of the option, used for matching. Defaults to the option's rendered text content. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- | |
selectedIcon
*
|
Array
|
- | |
start
*
|
Array
|
- | |
end
*
|
Array
|
- |