# Select

Picks one value, or several, from a fixed list the user cannot type into.

Select is a listbox behind a trigger, for picking one value or several from a
fixed set. Use it where you would reach for a native `<select>`; once the list is
long enough to scroll, use [`Combobox`](/components/combobox).

The example switches the portal off, so the popup renders in place and scrolls
with the frame around it rather than with the page.

::component-preview{name="SelectPreview"}
```vue
<template>
  <Select.Root v-model="framework" :options="options">
    <Select.Trigger
      class="group border-surface rounded-component-lg focus:focus-ring data-[popup-open=true]:focus-ring data-[disabled=true]:border-disabled-subtle flex h-12 w-64 cursor-pointer items-center justify-between gap-1.5 border-2 px-3.5 text-left outline-4 outline-transparent transition-all duration-100 ease-linear select-none data-[disabled=true]:cursor-not-allowed"
    >
      <span
        class="pointer-events-none flex max-h-full w-full flex-col-reverse justify-center gap-0 px-1 transition-all duration-100 ease-linear group-data-[filled=true]:gap-1"
      >
        <Select.Value
          class="text-primary-solid type-component-lg group-data-[disabled=true]:text-disabled-on-subtle flex h-0 w-full items-center overflow-hidden [line-height:normal]! opacity-0 transition-all duration-100 ease-linear group-data-[filled=true]:h-[0.9375rem] group-data-[filled=true]:opacity-100"
        />
        <span
          class="text-primary-muted type-component-lg group-data-[filled=true]:type-component-2xs group-data-[disabled=true]:text-disabled-muted flex w-full items-center [line-height:normal]! transition-all duration-100 ease-linear"
        >
          Framework
        </span>
      </span>

      <Select.Icon
        class="text-primary-muted group-data-[filled=true]:text-primary-solid group-data-[disabled=true]:text-disabled-muted flex shrink-0 items-center transition-all duration-100 ease-linear"
      >
        <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
          <path
            d="m4 6.5 4 4 4-4"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </Select.Icon>
    </Select.Trigger>

    <Select.Content
      class="rounded-component-2xl bg-primary-inverted shadow-component-high overflow-y-auto p-1.5 backdrop-blur-[8rem] [&_[data-scope=list]]:outline-none"
    >
      <Select.Item
        v-for="name in frameworks"
        :key="name"
        class="group/item rounded-component-md type-component-lg text-primary-on-subtle hover:bg-primary-subtle active:bg-primary-subtle-hover data-[highlighted=true]:bg-primary-subtle data-[disabled=true]:text-disabled-on-subtle flex h-12 w-full items-center justify-start gap-4 px-1 transition-colors duration-100 ease-linear select-none [--mirror-select-item-cursor:pointer]"
        :value="name"
      >
        <Select.ItemText
          class="inline-flex h-full min-w-0 flex-1 items-center overflow-hidden pr-1.5 pl-5.5"
        >
          <span class="truncate">{{ name }}</span>
        </Select.ItemText>

        <Select.ItemIndicator
          class="text-primary-muted group-hover/item:text-primary-solid group-data-[highlighted=true]/item:text-primary-solid group-data-[disabled=true]/item:text-disabled-muted flex shrink-0 items-center pr-4 leading-none transition-colors duration-100 ease-linear"
        >
          <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
            <path
              d="m3.5 8.5 3 3 6-6.5"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </Select.ItemIndicator>
      </Select.Item>
    </Select.Content>
  </Select.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Select } from '@maas/mirror/vue'

const options = {
  modal: false,
  portal: { disabled: true },
}

const frameworks = ['Nuxt', 'Astro', 'Remix', 'SvelteKit']
const framework = ref<string | null>('Nuxt')
</script>
```
::

## Usage guidelines

- Make sure the select has an accessible name, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on the trigger.
- `Select.Content` teleports to the end of `<body>` unless you turn that off
  with `options.portal.disabled`. Leave it on where the trigger sits inside
  anything with `overflow: hidden`.
- The content resolves everything but `side` and `align` once, during setup, so
  change the rest before the popup first renders or key the root to remount it.
- Once the list is long enough to scroll, reach for
  [`Combobox`](/components/combobox) instead, so the user can narrow it by
  typing.
- Where the OS picker is the better control, on a phone or a tablet, set
  [`options.native`](#native) and keep the same markup.

## Anatomy

Assemble the parts. `Select.Content` is one part over four boxes: the teleport,
the positioned element, the popup and the listbox. Everything you put inside it
renders in the listbox.

::component-anatomy
---
parts:
  - name: Select.Root
    required: true
    description: 'Renders its children plus one hidden input per selected value.'
    children:
      - name: Select.Trigger
        required: true
        description: 'Renders a <button>. Opens the popup and owns the collapsed state.'
        children:
          - name: Select.Value
            description: 'Renders a <span> with the selected item’s text.'
          - name: Select.Icon
            description: 'Renders a <span aria-hidden="true"> for the disclosure marker.'
      - name: Select.Content
        required: true
        description: 'Renders the teleport, the floating box, the popup and the <div role="listbox">.'
        children:
          - name: Select.Arrow
            description: 'Renders a <div aria-hidden="true"> pointing at the trigger. Goes in the header or footer slot.'
          - name: Select.ScrollUpArrow
            description: 'Renders a <div> that scrolls the list up while a pointer rests on it. Goes in the header slot.'
          - name: Select.ScrollDownArrow
            description: 'Renders a <div> that scrolls the list down. Goes in the footer slot.'
          - name: Select.Group
            description: 'Renders a <div role="group">.'
            children:
              - name: Select.GroupLabel
                description: 'Renders a <div>, wired to its group.'
          - name: Select.Item
            required: true
            description: 'Renders a <div role="option">.'
            children:
              - name: Select.ItemText
                description: 'Renders a <span> with the item’s label.'
              - name: Select.ItemIndicator
                description: 'Renders a <span> while the item is selected.'
          - name: Select.Separator
            description: 'Renders a <div role="separator">.'
---
::

```vue
<script setup lang="ts">
import { Field, Label, Select } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'framework' }">
    <Label>Framework</Label>
    <Select.Root v-model="framework">
      <Select.Trigger>
        <Select.Value placeholder="Pick one" />
        <Select.Icon />
      </Select.Trigger>
      <Select.Content>
        <Select.Group>
          <Select.GroupLabel>Meta</Select.GroupLabel>
          <Select.Item value="react">
            <Select.ItemText>React</Select.ItemText>
            <Select.ItemIndicator />
          </Select.Item>
        </Select.Group>
        <Select.Separator />
        <Select.Item value="vue">
          <Select.ItemText>Vue</Select.ItemText>
          <Select.ItemIndicator />
        </Select.Item>
      </Select.Content>
    </Select.Root>
  </Field.Root>
</template>
```

## Examples

### Multiple selection

Set `options.multiple` and the value becomes an array, the popup stays open as
you pick, and every part gains `data-multiple`.

```vue
<script setup lang="ts">
const frameworks = ref<Array<string>>([])
</script>

<template>
  <Select.Root v-model="frameworks" :options="{ multiple: true }">
    <Select.Trigger>
      <Select.Value placeholder="Pick a few" />
    </Select.Trigger>
  </Select.Root>
</template>
```

The value and `options.multiple` have to agree, or the root throws
`invalid_multiple_value`.

`Select.Value` joins the labels with a comma. Take its slot to render them some
other way.

```vue
<template>
  <Select.Value v-slot="{ value }" placeholder="Pick a few">
    {{ value.length }} selected
  </Select.Value>
</template>
```

### Object values

An item value does not have to be a string. Hand `Select.Item` the object you
already hold and tell the select how to read it: `isItemEqualToValue` decides
which item the current selection is, `itemToStringLabel` supplies the text
`Select.Value` shows, and `itemToStringValue` supplies the string the hidden
inputs submit.

```vue
<script setup lang="ts">
interface Framework {
  id: string
  name: string
}

const frameworks: Array<Framework> = [
  { id: 'vue', name: 'Vue' },
  { id: 'react', name: 'React' },
]

const framework = ref<Framework | null>(null)
</script>

<template>
  <Select.Root
    v-model="framework"
    name="framework"
    :options="{
      isItemEqualToValue: (item, value) => item.id === value.id,
      itemToStringLabel: (item) => item.name,
      itemToStringValue: (item) => item.id,
    }"
  >
    <Select.Trigger>
      <Select.Value placeholder="Pick one" />
    </Select.Trigger>
    <Select.Content>
      <Select.Item v-for="entry in frameworks" :key="entry.id" :value="entry">
        <Select.ItemText>{{ entry.name }}</Select.ItemText>
      </Select.Item>
    </Select.Content>
  </Select.Root>
</template>
```

Without `isItemEqualToValue` two objects only match when they are the same
object, which is usually what you want when the items and the model come from
the same array. Set it as soon as the model is rebuilt from a server response.

An object shaped `{ value, label }` needs neither converter: the defaults read
`label` for the text and `value` for the form.

### Native

Set `options.native` and the select runs on the browser’s own `<select>`, which
brings the native popup, the OS picker on a phone or a tablet, and the keyboard
handling that comes with it. Your markup does not change. The trigger keeps
drawing the frame you styled and hosts a transparent `<select>` over it, and
`Select.Content` stops rendering a popup and only mounts the items, which is
where the options come from.

::component-preview{name="SelectNativePreview"}
```vue
<template>
  <Select.Root v-model="framework" :options="options">
    <Select.Trigger
      class="group border-surface rounded-component-lg data-[focused=true]:focus-ring flex h-12 w-64 items-center justify-between gap-1.5 border-2 px-3.5 text-left outline-4 outline-transparent transition-all duration-100 ease-linear select-none"
    >
      <Select.Value
        class="text-primary-solid type-component-lg data-[placeholder=true]:text-primary-muted flex min-w-0 items-center truncate"
        placeholder="Pick a framework"
      />

      <Select.Icon
        class="text-primary-muted flex shrink-0 items-center transition-all duration-100 ease-linear"
      >
        <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
          <path
            d="m4 6.5 4 4 4-4"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </Select.Icon>
    </Select.Trigger>

    <Select.Content>
      <Select.Item v-for="name in frameworks" :key="name" :value="name">
        {{ name }}
      </Select.Item>
    </Select.Content>
  </Select.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Select } from '@maas/mirror/vue'

const options = { native: true }

const frameworks = ['Nuxt', 'Astro', 'Remix', 'SvelteKit']
const framework = ref<string | null>(null)
</script>
```
::

The `<select>` is the control now. It takes the focus, the tab stop, the name,
the form and the `required` flag, so `Select.Root` renders no hidden inputs and
a `<label for>` points at it. Focus still reaches the trigger’s `data-focused`,
which is what the ring in the example is drawn from, since the element that
holds focus is the one inside the frame rather than the frame itself.

Every part that resolves the select context gains `data-native`, so a native
select is styled as its own mode.

```css
.trigger[data-native='true'] {
  padding-right: 2rem;
}
```

`Select.Group` becomes an `<optgroup>`, labelled with the text
`Select.GroupLabel` rendered. A group without a label stays flat, and so does
everything outside a group.

```vue
<template>
  <Select.Content>
    <Select.Group>
      <Select.GroupLabel>Meta</Select.GroupLabel>
      <Select.Item value="react">React</Select.Item>
    </Select.Group>
    <Select.Item value="vue">Vue</Select.Item>
  </Select.Content>
</template>
```

Two things are left behind. The popup never opens, so `open`, `defaultOpen`,
`update:open` and `data-popup-open` are all inert, the `header` and `footer`
slots are not rendered at all, and `Select.ItemIndicator` and
`Select.Separator` mount inside the registry where nobody sees them. And
`Select.Trigger` renders its own element: `asChild` is ignored and the default
`<button>` becomes a `<div>`, because a `<button>` may not contain a
`<select>`.

Object values keep working. An `<option>` carries a string, and that string
comes from `itemToStringValue`, the same one the hidden inputs would have
submitted.

### Positioning

`Select.Content` positions itself against the trigger, steered by the
`floating` keys on the root’s `options`; the options table below lists every
one of them. The resolved placement comes back
as `data-side` and `data-align`; read those rather than the options you passed,
since flipping can change either one.

```vue
<template>
  <Select.Root
    :options="{
      floating: { side: 'top', align: 'center', sideOffset: 8, flip: false },
    }"
  />
</template>
```

Pass `anchor` to position against something else, and pass any of the ten
floating keys straight to the content to override the options for one instance.

```vue
<template>
  <Select.Content :anchor="() => frame" side="top" :side-offset="12" />
</template>
```

The content writes `--rack-floating-anchor-width`,
`--rack-floating-anchor-height`, `--rack-floating-available-width`,
`--rack-floating-available-height` and `--rack-floating-transform-origin` onto
the floating box, which is why an unstyled popup is already as wide as its
trigger and never taller than the space around it.

```css
.popup {
  min-width: var(--mirror-select-popup-min-width);
  max-height: var(--mirror-select-popup-max-height);
}
```

### An arrow

`Select.Arrow` draws a marker pointing at the trigger. Render it in the
content’s `header` or `footer` slot, so it sits inside the popup and outside
the listbox. It ships `position: absolute` and takes its offset from the same
pass that placed the popup, so give it a shape and leave the placement alone.

```vue
<template>
  <Select.Content>
    <template #header>
      <Select.Arrow class="arrow" />
    </template>
    <Select.Item value="react">React</Select.Item>
  </Select.Content>
</template>
```

`options.floating.arrowPadding` says how close the arrow may come to the
popup’s corners. Once collision handling has pushed it off the trigger’s
centre it carries `data-uncentered`, and it carries the resolved `data-side`
and `data-align` throughout.

### Aligning a value with the trigger

Set `options.floating.alignItemWithTrigger` to lay the selected item over the
trigger. The popup covers the trigger rather than sitting beside it, and a list
too tall for the screen scrolls until the selected value meets the button that
opened it.

```vue
<template>
  <Select.Root :options="{ floating: { alignItemWithTrigger: true } }" />
</template>
```

The measurement is taken against the viewport, so while it holds the floating
box takes fixed coordinates and carries `data-item-aligned`, and `side`,
`sideOffset`, `align`, `alignOffset` and `strategy` have no effect. Style the
covering popup from the attribute rather than from the option.

```css
.mirror-select-floating[data-item-aligned='true'] .mirror-select-popup {
  box-shadow: 0 0 0 1px var(--color-border-muted);
}
```

Three things send the popup back to the standard placement, and each of them
leaves `data-item-aligned` off rather than reporting anything: nothing is
selected, the pointer is a coarse one, and the popup has not been laid out yet.
Leave an arrow off a select that aligns its items, since the popup covers the
trigger it would point at.

### Scrolling a long list

`Select.ScrollUpArrow` and `Select.ScrollDownArrow` move a list that is taller
than the popup. Each one renders only while the list can still travel that way,
and each scrolls for as long as a pointer rests on it or holds it down. Put the
up arrow in the content’s `header` slot and the down arrow in its `footer`
slot, so both sit inside the popup and outside the listbox.

```vue
<template>
  <Select.Content>
    <template #header>
      <Select.ScrollUpArrow class="scroll-arrow" />
    </template>
    <Select.Item value="react">React</Select.Item>
    <template #footer>
      <Select.ScrollDownArrow class="scroll-arrow" />
    </template>
  </Select.Content>
</template>
```

Give the listbox an overflow of its own, or put it on the popup, and cap the
height. The arrows measure whichever of the two boxes scrolls.

```css
.mirror-select-list {
  overflow-y: auto;
}
```

Each arrow publishes `data-direction`, so one rule paints both and a second
turns the glyph around.

```css
.scroll-arrow[data-direction='down'] {
  rotate: 180deg;
}
```

### Where the popup renders

The content teleports to the end of `<body>`, which is what keeps a popup out
of an ancestor’s `overflow: hidden` and above the rest of the page. Turn it off
to render in place, or name a target of your own.

```vue
<template>
  <Select.Root :options="{ portal: { to: '#overlays' } }" />
  <Select.Root :options="{ portal: { disabled: true } }" />
</template>
```

`options.backdrop` adds a fixed `<div role="presentation">` behind the popup.
It has no dismiss behaviour of its own: the popup’s layer already closes on a
pointer down outside.

```vue
<template>
  <Select.Root :options="{ backdrop: true }" />
</template>
```

Under `options.native` none of this runs. There is no popup to place, teleport
or transition, so the floating and portal options are ignored, the open state
never leaves `false`, `update:open` never fires and `data-popup-open` never
appears. The browser owns the list.

### Inside a form

`Select.Root` renders one visually hidden input per selected value once it has a
name, so [the form sees it](/components/form-integration#form-participation), and a surrounding `Field` supplies
the name and the flags.

```vue
<template>
  <Field.Root :options="{ name: 'framework', required: true }">
    <Label>Framework</Label>
    <Select.Root v-model="framework">
      <Select.Trigger>
        <Select.Value placeholder="Pick one" />
      </Select.Trigger>
    </Select.Root>
  </Field.Root>
</template>
```

### Reaching the select from anywhere

Give the root an `id` and `useMirrorSelect(id)` opens, closes and reads that
select from anywhere in the app.

```vue
<script setup lang="ts">
const { isOpen, open, value, setValue } = useMirrorSelect(SelectId.Framework)
</script>

<template>
  <Button :aria-expanded="isOpen" @click="open()">Choose a framework</Button>
</template>
```

### Styling from state

Every part writes its state to `data-*`, so style it with attribute selectors.

```css
.item[data-highlighted='true'] {
  background: var(--app-color-primary-bg-subtle);
}

.item[data-disabled='true'] {
  opacity: 0.5;
}

.icon[data-popup-open='true'] {
  transform: rotate(180deg);
}

.value[data-placeholder='true'] {
  color: var(--app-color-surface-fg-subtle);
}
```

The content’s four boxes are not parts, so they are reached by their
`data-scope` or by the class each one ships. A `class` on `Select.Content` is
applied to the popup, the box you paint.

```vue
<template>
  <Select.Content class="popup" />
</template>
```

```css
.mirror-select-floating {
  --mirror-select-floating-z-index: 80;
}

[data-scope='list'] {
  outline: none;
}
```

## API reference

**Module.** A bundled `options` object on the root, and `useMirrorSelect(id)` as
the programmatic API. Every part that reads the select takes `id` to resolve one
it is not nested inside.

### `Select.Root`

Renders its children plus one visually hidden `<input>` per selected value.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: '[The instance ID](#reaching-the-select-from-anywhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The selected value or values. `v-model`.
      - label: 'string | number | object | Array | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: 'Initial selection when [uncontrolled](/components/composition#controlled-and-uncontrolled).'
      - label: 'string | number | object | Array | null'
      - label: 'null'
  - items:
      - label: open
        description: Open state. `v-model:open`.
      - label: 'boolean | undefined'
      - label: undefined
  - items:
      - label: defaultOpen
        description: 'Initial open state when [uncontrolled](/components/composition#controlled-and-uncontrolled).'
      - label: boolean
      - label: 'false'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: SelectOptions
      - label: see below
        plaintext: true
---
::

`disabled`, `readOnly`, `required` and `name` resolve in one order everywhere:
the `options` object first, then the surrounding `Field`, then `false`. Leave
the option unset and the field answers; set it, `false` included, and it wins.

Control is decided once, on the first render. A select that mounts with
`modelValue` bound stays controlled even if the binding later turns
`undefined`, and one that mounts without it stays uncontrolled. The same holds
for `open`, so swapping a binding in or out after mount changes nothing.

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: multiple
        description: 'Turns the value into an [array](#multiple-selection).'
      - label: boolean
      - label: 'false'
  - items:
      - label: modal
        description: Traps focus in the popup and marks the rest of the document `inert`.
      - label: boolean
      - label: 'true'
  - items:
      - label: loop
        description: Arrow keys wrap at the ends.
      - label: boolean
      - label: 'true'
  - items:
      - label: disabled
        description: Disables the trigger and refuses opening. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: readOnly
        description: Refuses selection, keeps focus. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: required
        description: Marks the hidden inputs required. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: name
        description: Name for the hidden inputs. Falls to the `Field` when unset.
      - label: string
      - label: undefined
  - items:
      - label: form
        description: ID of the form the hidden inputs belong to.
      - label: string
      - label: undefined
  - items:
      - label: isItemEqualToValue
        description: 'Decides whether an item value and a [selected value](#object-values) are the same one.'
      - label: '(item, value) => boolean'
      - label: Object.is
        plaintext: true
  - items:
      - label: itemToStringLabel
        description: 'Turns an item value into the text `Select.Value` shows.'
      - label: '(item) => string'
      - label: see below
        plaintext: true
  - items:
      - label: itemToStringValue
        description: 'Turns an item value into the string the hidden inputs submit.'
      - label: '(item) => string'
      - label: see below
        plaintext: true
  - items:
      - label: typeahead
        description: Highlights the first item whose label starts with the typed string.
      - label: 'boolean | { resetAfter?: number }'
      - label: '{ resetAfter: 1000 }'
  - items:
      - label: highlightOnHover
        description: The pointer moves the highlight.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.escape
        description: Escape closes the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.pointerDownOutside
        description: A pointer down outside closes the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.focusOutside
        description: Focus leaving closes the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: focus.trapped
        description: Focus is trapped in the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: focus.restore
        description: Closing returns focus to the trigger.
      - label: boolean
      - label: 'true'
  - items:
      - label: backdrop
        description: 'Renders a fixed `<div role="presentation">` behind the popup.'
      - label: boolean
      - label: 'false'
  - items:
      - label: native
        description: 'Runs the select on [the browser’s own `<select>`](#native).'
      - label: boolean
      - label: 'false'
  - items:
      - label: portal.to
        description: The teleport target for the content.
      - label: 'string | HTMLElement'
      - label: body
        plaintext: true
  - items:
      - label: portal.disabled
        description: Renders the content in place instead.
      - label: boolean
      - label: 'false'
  - items:
      - label: portal.defer
        description: 'Resolves the target after mount, for a target rendered by the same app.'
      - label: boolean
      - label: 'false'
  - items:
      - label: floating.side
        description: Preferred side. Reactive.
      - label: '''top'' | ''right'' | ''bottom'' | ''left'''
      - label: '''bottom'''
  - items:
      - label: floating.sideOffset
        description: Gap from the trigger, in pixels.
      - label: number
      - label: '4'
  - items:
      - label: floating.align
        description: Alignment along that side. Reactive.
      - label: '''start'' | ''center'' | ''end'''
      - label: '''start'''
  - items:
      - label: floating.alignOffset
        description: Shift along the alignment axis, in pixels.
      - label: number
      - label: '0'
  - items:
      - label: floating.strategy
        description: Positioning strategy.
      - label: '''absolute'' | ''fixed'''
      - label: '''absolute'''
  - items:
      - label: floating.flip
        description: Flip to the opposite side when there is no room.
      - label: boolean
      - label: 'true'
  - items:
      - label: floating.shift
        description: Slide along the side to stay in view.
      - label: boolean
      - label: 'true'
  - items:
      - label: floating.collisionPadding
        description: Padding used by flipping and shifting.
      - label: number
      - label: '8'
  - items:
      - label: floating.alignItemWithTrigger
        description: 'Lays [the selected item](#aligning-a-value-with-the-trigger) over the trigger instead of placing the popup beside it.'
      - label: boolean
      - label: 'false'
  - items:
      - label: floating.arrowPadding
        description: How close `Select.Arrow` may come to the corners of the popup.
      - label: number
      - label: '5'
  - items:
      - label: forceMount.popup
        description: Keeps the popup and the backdrop mounted while closed.
      - label: boolean
      - label: 'false'
  - items:
      - label: transition.popup
        description: Vue transition name for the popup.
      - label: string
      - label: '''mirror-select-popup'''
  - items:
      - label: transition.backdrop
        description: Vue transition name for the backdrop.
      - label: string
      - label: '''mirror-select-backdrop'''
---
::

Both string converters read an item the same way. A primitive is its own
string, an object shaped `{ value, label }` supplies the key the converter
needs, and anything else falls back to `String(item)`.

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: 'The selection changes, `null` included.'
      - label: 'string | number | object | Array | null'
  - items:
      - label: update:open
        description: The popup opens or closes.
      - label: boolean
  - items:
      - label: highlightChange
        description: The highlight moves, by pointer, arrow key or typeahead.
      - label: 'string | number | object | null'
---
::

### `Select.Trigger`

Renders a `<button>` with `aria-haspopup="listbox"`. Under `options.native` it
renders a `<div>` around a transparent `<select>` instead, drops the listbox
wiring, and ignores `asChild`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: disabled
        description: Escape hatch over the options.
      - label: boolean
      - label: options.disabled
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus the four below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: open
        description: Mirrors `data-popup-open`.
      - label: boolean
  - items:
      - label: value
        description: The selected value or values.
      - label: 'SelectModelValue | null'
  - items:
      - label: side
        description: Mirrors `data-side`.
      - label: 'SelectSide | undefined'
  - items:
      - label: align
        description: Mirrors `data-align`.
      - label: 'SelectAlign | undefined'
  - items:
      - label: pressed
        description: Mirrors `data-pressed`.
      - label: boolean
---
::

### `Select.Value`

Renders a `<span>` with the selected item’s text, joining a multiple selection
with a comma.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: placeholder
        description: Rendered when nothing is selected.
      - label: string
      - label: ''''''
---
::

#### Slots

::docs-table
---
columns:
  - label: Slot
  - label: Slot props
rows:
  - items:
      - label: default
        description: Replaces the whole label, placeholder included.
      - label: 'value, label'
  - items:
      - label: placeholder
        description: Replaces the `placeholder` prop when nothing is selected.
      - label: none
        plaintext: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The selected value or values.
      - label: 'SelectModelValue | null'
  - items:
      - label: label
        description: The joined label text.
      - label: string
---
::

### `Select.Icon`

Renders a `<span aria-hidden="true">` with `data-popup-open`, for the disclosure
marker. No props beyond `id` and the primitive ones.

### `Select.Content`

Renders everything that floats: the teleport, the floating box, the popup and
the listbox. Everything in its default slot renders inside the listbox. It
anchors on `Select.Trigger`. Under `options.native` it renders one hidden,
inert box instead, and the items inside it feed the browser’s own list.

#### Layers

None of them is a part, so each is styled through its `data-scope` or the class
it ships. A `class` on the content itself is applied to the popup, or to the
registry under `options.native`.

::docs-table
---
columns:
  - label: Layer
  - label: Element
  - label: Publishes
rows:
  - items:
      - label: portal
        description: The teleport. Configured through `options.portal`.
      - label: none
        plaintext: true
      - label: nothing
        plaintext: true
  - items:
      - label: backdrop
        description: '`.mirror-select-backdrop`, only under `options.backdrop`.'
      - label: 'div[role=presentation]'
      - label: '`data-select`, `data-state`'
        plaintext: true
  - items:
      - label: floating
        description: '`.mirror-select-floating`. Carries the floating styles and the `--rack-floating-*` set.'
      - label: div
      - label: '`data-side`, `data-align`, `data-anchor-hidden`, `data-state`'
        plaintext: true
  - items:
      - label: popup
        description: '`.mirror-select-popup`. Presence, dismissal and the modal behaviour live here.'
      - label: div
      - label: '`data-select`, `data-side`, `data-align`, `data-state`, `data-multiple`'
        plaintext: true
  - items:
      - label: list
        description: '`.mirror-select-list`. Takes focus while the popup is open, and its generated ID is what the trigger points `aria-controls` at.'
      - label: 'div[role=listbox]'
      - label: '`data-multiple`, `aria-activedescendant`, `aria-multiselectable`'
        plaintext: true
  - items:
      - label: registry
        description: '`.mirror-select-registry`, the only layer under `options.native`. Hidden and inert, so the items register without being seen.'
      - label: 'div[aria-hidden][inert]'
      - label: '`data-select`, `data-native`'
        plaintext: true
---
::

#### Props

Every prop below but `anchor` is an escape hatch over an option, `forceMount`
over `options.forceMount.popup` and the rest over the matching
`options.floating` key, and only `side` and `align` stay reactive after mount.

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: forceMount
        description: Keeps the popup and the backdrop mounted while closed.
      - label: boolean
      - label: options.forceMount.popup
        plaintext: true
  - items:
      - label: anchor
        description: Position against this element instead.
      - label: 'HTMLElement | (() => HTMLElement | null)'
      - label: the trigger
        plaintext: true
  - items:
      - label: side
        description: Preferred side.
      - label: '''top'' | ''right'' | ''bottom'' | ''left'''
      - label: '''bottom'''
  - items:
      - label: sideOffset
        description: Gap from the trigger, in pixels.
      - label: number
      - label: '4'
  - items:
      - label: align
        description: Alignment along that side.
      - label: '''start'' | ''center'' | ''end'''
      - label: '''start'''
  - items:
      - label: alignOffset
        description: Shift along the alignment axis, in pixels.
      - label: number
      - label: '0'
  - items:
      - label: strategy
        description: Positioning strategy.
      - label: '''absolute'' | ''fixed'''
      - label: '''absolute'''
  - items:
      - label: flip
        description: Flip to the opposite side when there is no room.
      - label: boolean
      - label: 'true'
  - items:
      - label: shift
        description: Slide along the side to stay in view.
      - label: boolean
      - label: 'true'
  - items:
      - label: collisionPadding
        description: Padding used by flipping and shifting.
      - label: number
      - label: '8'
  - items:
      - label: alignItemWithTrigger
        description: Lays the selected item over the trigger.
      - label: boolean
      - label: 'false'
  - items:
      - label: arrowPadding
        description: How close the arrow may come to the corners of the popup.
      - label: number
      - label: '5'
---
::

#### Slots

::docs-table
---
columns:
  - label: Slot
  - label: Renders
rows:
  - items:
      - label: default
      - label: 'Inside the listbox. Items, groups and separators.'
        plaintext: true
  - items:
      - label: header
      - label: Inside the popup, above the listbox. Anything that is not an option.
        plaintext: true
  - items:
      - label: footer
      - label: 'Inside the popup, below the listbox.'
        plaintext: true
---
::

#### Slot props

All three slots take the same props.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: open
        description: Mirrors `data-state`.
      - label: boolean
  - items:
      - label: side
        description: Resolved after collision handling.
      - label: SelectSide
  - items:
      - label: align
        description: Resolved after collision handling.
      - label: SelectAlign
---
::

### `Select.Arrow`

Renders a `<div aria-hidden="true">` pointing at the trigger. It ships
`position: absolute` and takes its offset from the same positioning pass as the
popup; the shape it draws is yours. Render it in the content’s `header` or
`footer` slot, so it sits inside the popup and outside the listbox, and expect
`missing_select_content` anywhere else. No props beyond `id` and the primitive
ones.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: side
        description: Mirrors `data-side`.
      - label: SelectSide
  - items:
      - label: align
        description: Mirrors `data-align`.
      - label: SelectAlign
  - items:
      - label: uncentered
        description: Mirrors `data-uncentered`.
      - label: boolean
---
::

### `Select.ScrollUpArrow`

Renders a `<div aria-hidden="true">` that scrolls the list up for as long as a
pointer rests on it or holds it down. It renders only while the list has
somewhere left to travel upwards, and nothing at all under `options.native`.
Render it in the content’s `header` slot, and expect `missing_select_content`
outside a `Select.Content`. No props beyond `id` and the primitive ones.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: direction
        description: Mirrors `data-direction`. Always `'up'` here.
      - label: SelectScrollDirection
  - items:
      - label: side
        description: Mirrors `data-side`.
      - label: SelectSide
---
::

### `Select.ScrollDownArrow`

Renders a `<div aria-hidden="true">` that scrolls the list down on the same
terms, rendering only while the list has somewhere left to travel downwards.
Render it in the content’s `footer` slot. No props beyond `id` and the
primitive ones.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: direction
        description: Mirrors `data-direction`. Always `'down'` here.
      - label: SelectScrollDirection
  - items:
      - label: side
        description: Mirrors `data-side`.
      - label: SelectSide
---
::

### `Select.Item`

Renders a `<div role="option">`. Items register in document order, so grouping
and portals cannot scramble the arrow-key sequence. Naming a `<button>` in `as`
gets the button semantics with it, including the native `disabled` attribute.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: value
        description: 'Required. The value this item selects, [an object included](#object-values).'
      - label: 'string | number | object'
      - label: none
        plaintext: true
  - items:
      - label: label
        description: Used by typeahead and by `Select.Value`, where the item renders more than text.
      - label: string
      - label: the `Select.ItemText` content
        plaintext: true
  - items:
      - label: disabled
        description: Not selectable, skipped by the arrow keys and by typeahead.
      - label: boolean
      - label: 'false'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: selected
        description: Mirrors `data-selected`.
      - label: boolean
  - items:
      - label: highlighted
        description: Mirrors `data-highlighted`.
      - label: boolean
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
  - items:
      - label: index
        description: Mirrors `data-index`.
      - label: number
---
::

### `Select.ItemText`

Renders a `<span>` and registers its text as the item’s label when no `label`
prop was given.

### `Select.ItemIndicator`

Renders a `<span>` while the item is selected.

#### Props

One indicator per item, so `forceMount` is configured on the part and nowhere
else. Set it on the indicator you want kept in the tree while its item is
unselected. Every other indicator is left where it was.

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: forceMount
        description: Keeps it mounted while unselected.
      - label: boolean
      - label: 'false'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: selected
        description: Mirrors `data-selected`.
      - label: boolean
---
::

### `Select.Group`

Renders a `<div role="group">`.

### `Select.GroupLabel`

Renders a `<div>`, wired to its group through `aria-labelledby`. It must be
nested inside a `Select.Group`.

### `Select.Separator`

Renders a `<div role="separator">` with `aria-orientation`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: orientation
        description: The value of `aria-orientation`.
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
---
::

### Composable

`useMirrorSelect(id)` reaches a select from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: isOpen
        description: '`true` while the popup is open.'
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: open
        description: Opens the popup and seeds the highlight.
      - label: () => void
        escape: true
  - items:
      - label: close
        description: Closes the popup and drops the highlight.
      - label: '(restoreFocus?: boolean) => void'
        escape: true
  - items:
      - label: toggle
        description: Opens or closes.
      - label: () => void
        escape: true
  - items:
      - label: value
        description: The selected value or values.
      - label: 'ComputedRef<SelectModelValue | null>'
        escape: true
  - items:
      - label: setValue
        description: Sets the selection.
      - label: '(next: SelectModelValue | null) => void'
        escape: true
  - items:
      - label: highlightedValue
        description: The highlighted item’s value.
      - label: 'ComputedRef<SelectValueType | null>'
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_select_context
        description: A `Select` part rendered outside `Select.Root` and received no `id`.
  - items:
      - label: missing_select_content
        description: A `Select.Arrow`, `Select.ScrollUpArrow` or `Select.ScrollDownArrow` rendered outside `Select.Content`.
  - items:
      - label: missing_context
        description: A `Select.ItemText` or `Select.ItemIndicator` rendered outside `Select.Item`, or a `Select.GroupLabel` outside `Select.Group`.
  - items:
      - label: missing_item_value
        description: A `Select.Item` rendered without a `value`.
  - items:
      - label: duplicate_item_value
        description: Two items in the same select declared the same `value`.
  - items:
      - label: invalid_multiple_value
        description: '`options.multiple` is set and the value is not an array, or is unset and the value is.'
---
::

## Accessibility

The trigger is a `<button>` with `aria-haspopup="listbox"`, `aria-expanded` and
`aria-controls`, and the list is a `role="listbox"` with `aria-activedescendant`
pointing at the highlighted item, so focus sits on the list rather than on any
item.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: '`ArrowDown` / `ArrowUp` on the trigger'
        plaintext: true
      - label: Opens the popup and highlights the selected item, or the first enabled one.
        plaintext: true
  - items:
      - label: '`Enter` / `Space` on the trigger'
        plaintext: true
      - label: Toggles the popup. On a non-native trigger, Space acts on `keyup`.
        plaintext: true
  - items:
      - label: '`ArrowDown` / `ArrowUp` in the popup'
        plaintext: true
      - label: Moves the highlight, wrapping while `options.loop` is on.
        plaintext: true
  - items:
      - label: '`Home` / `End` in the popup'
        plaintext: true
      - label: Highlights the first or last enabled item, ignoring `loop`.
        plaintext: true
  - items:
      - label: '`Enter` in the popup'
        plaintext: true
      - label: Selects the highlighted item and closes, returning focus to the trigger. Under `multiple` it toggles and stays open.
        plaintext: true
  - items:
      - label: '`Space` in the popup'
        plaintext: true
      - label: As `Enter`. During a typeahead search the space is consumed as a character.
        plaintext: true
  - items:
      - label: Escape
      - label: Closes the popup and returns focus to the trigger.
        plaintext: true
  - items:
      - label: Tab
      - label: Selects the highlighted item, closes, and lets focus move on.
        plaintext: true
  - items:
      - label: Printable characters in the popup
        plaintext: true
      - label: Typeahead. Highlights the first enabled item whose label starts with the typed string, resetting after `resetAfter`.
        plaintext: true
  - items:
      - label: Printable characters on the closed trigger
        plaintext: true
      - label: Typeahead again, this time selecting outright the way a native select does. Nothing is registered while the popup is unmounted, so the first keystroke opens the list and the search carries on there unless `Select.Content` has `force-mount`.
        plaintext: true
---
::

Every keyboard move scrolls the highlighted item into view, so a popup capped by
`--mirror-select-popup-max-height` cannot lose the highlight off screen. A
pointer highlight leaves the scroll position where the reader put it.

`PageUp`, `PageDown`, `ArrowLeft` and `ArrowRight` are left to the browser.

Under `options.native` the whole table is left to the browser: the `<select>` is
the control, so it carries the focus, the tab stop and every key, and the
trigger around it carries no role and no tabindex of its own. `Select.Value`
goes `aria-hidden` there, since the `<select>` already announces the selection.
