Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

EmSelect

Edit this page
Report a Bug
Open issues
Copy Markdown

Renders elementIntermediateAug 14, 2026

A single- or multi-select listbox, composed from express parts so the trigger and the options are yours to shape.

Usage

Unlike EmButton and EmTextField, EmSelect has a variable tree — you decide what the trigger shows and what an option looks like — so it ships as a compound rather than a shell. Five parts, each a real component:

EmSelect owns the value and the open state. EmSelectActivator is the trigger. EmSelectValue renders the current selection and EmSelectPlaceholder renders when there is none. EmSelectContent is the popover, and EmSelectItem is one option in it.

label stays a prop on the root, because a field label has fixed anatomy even when the control does not.

<script setup lang="ts">
  import {
    EmSelect,
    EmSelectActivator,
    EmSelectContent,
    EmSelectItem,
    EmSelectPlaceholder,
    EmSelectValue,
  } from '@paper/emerald'

  import { shallowRef } from 'vue'

  const region = shallowRef<string>()

  const regions = ['Americas', 'Europe', 'Asia Pacific', 'Middle East']
</script>

<template>
  <div class="emerald-docs-stack">
    <EmSelect v-model="region" label="Region">
      <EmSelectActivator>
        <EmSelectValue />

        <EmSelectPlaceholder>Choose a region</EmSelectPlaceholder>
      </EmSelectActivator>

      <EmSelectContent>
        <EmSelectItem v-for="item in regions" :key="item" :value="item">
          {{ item }}
        </EmSelectItem>
      </EmSelectContent>
    </EmSelect>
  </div>
</template>

<style>
  .emerald-docs-stack {
    display: flex;
    flex-direction: column;
    gap: var(--emerald-spacing-m, 16px);
    max-width: 320px;
  }
</style>

Anatomy

vue
<script setup lang="ts">
  import {
    EmSelect,
    EmSelectActivator,
    EmSelectContent,
    EmSelectItem,
    EmSelectPlaceholder,
    EmSelectValue,
  } from '@paper/emerald'
</script>

<template>
  <EmSelect>
    <EmSelectActivator>
      <EmSelectValue />

      <EmSelectPlaceholder />
    </EmSelectActivator>

    <EmSelectContent>
      <EmSelectItem />
    </EmSelectContent>
  </EmSelect>
</template>

Composed on v0

Every part maps one-to-one onto v0’s Select compound — Select.Root, Select.Activator, Select.Value, Select.Placeholder, Select.Content, Select.Item. Emerald adds CSS and a caret; v0 supplies everything else.

“Everything else” is most of what a select is. Select.Root is built on v0’s selection primitives, so multiple and mandatory are createSelection behaviors rather than props Emerald implements. The listbox roles, the roving aria-activedescendant, and the keyboard map are v0’s. Select.Content renders through the native popover API, which is why the menu escapes overflow and stacking contexts without a floating library or a z-index to manage.

Select is a picker over a fixed set of options — it has no text entry and no type-to-filter. When you need the reader to type, either to filter a long list or to enter a value that is not in it, reach for Combobox instead; that is where v0 puts filtering and typeahead. Emerald does not wrap it yet.

One structural note: Select.Root is renderless, so the element you see is a plain <div class="emerald-select"> that Emerald renders itself, with the <label> beside it pointing at the activator. That is why label is a prop on the root — there is a real element there to own it.

EmSelectActivator also appends the caret itself. It is Emerald’s, not yours, which keeps the chevron consistent across every select in an app; it is pinned to 16px by a host rule rather than by a prop, taking advantage of EmIcon’s zero-specificity sizing.

Examples

Multiple selection

multiple turns the model into an array and lets items accumulate. The type follows: v-model is T for a single select and T[] here, so bind a ref([]), matching the house rule that arrays and objects get ref and primitives get shallowRef.

Nothing about the binding forces that choice — each toggle assigns a fresh array rather than mutating the one you passed, so a shallowRef would track it too. Prefer ref for the array you own and read elsewhere, and reach for shallowRef only when you have measured a reason to.

The part that needs a decision from you is the trigger. EmSelectValue’s default content is the single selectedValue, which is not meaningful once there are several, so take its slot props instead. The slot hands out selectedValue and selectedValues, selectedItem and selectedItems, and a hasValue boolean — enough to render a comma list, a count, or a row of tags without tracking the selection separately.

EmSelectPlaceholder still handles the empty case; it renders exactly when nothing is selected, so you do not need a v-if on your own summary.

Reach for mandatory on the root when the field must never be empty — it stops the last item being deselected, which is a nicer constraint than a rule that rejects an empty array after the fact.

Rich options

EmSelectItem takes a default slot, so an option can hold anything — an icon, a secondary line, a badge. value is what lands in the model, and it is independent of what the option renders, so the display can be as rich as you like while the bound value stays a plain id.

The trade-off to be aware of is the trigger. Because value is the model, EmSelectValue can only render what you give it; with structured options you generally want to look the selected value back up, as this example does, rather than print the raw id. That lookup is yours — v0 tracks selection, not your data model.

disabled on an item keeps it visible and announced but unselectable, which is right when its absence would be confusing — a plan the account has outgrown, a permission the current role cannot grant. When an option is simply irrelevant, filter it out instead; a list of options that cannot be picked is noise a keyboard user has to walk through.

Keep the option’s text as its accessible name. Icons inside items should stay decorative — the label beside them already names the choice.

Props

EmSelect is generic over the option value type, T, defaulting to unknown.

PropTypeDefaultDescription
v-modelT | T[]Selected value, or values when multiple
labelstringVisible field label, associated with the activator
multiplebooleanfalseAccumulate selections into an array
mandatoryboolean | 'force'falsePrevent emptying the selection
disabledbooleanfalseField unavailable
namestringForm field name
formstringAssociate with a form by id
idstringgeneratedField id. Falls back to useId()
namespacestringWhich v0 Select instance to bind to. Only needed when nesting

Parts

Every part takes an optional namespace; only EmSelectItem adds props of its own. Its value is v0’s IDstring | number — not the root’s generic T, so an option keyed by an object needs an id here and the lookup back to the object stays yours.

PartRendersPropsSlot props
EmSelectActivatorThe trigger, plus Emerald’s caret
EmSelectValueThe current selectionselectedItem, selectedItems, selectedValue, selectedValues, hasValue, attrs
EmSelectPlaceholderShown while nothing is selected
EmSelectContentThe popover listbox
EmSelectItemOne optionvalue: ID (required), disabled: boolean

Accessibility

The listbox semantics, the focus model and the keyboard map all come from v0’s Select, so they match every other consumer of that compound rather than being Emerald’s own interpretation.

Keyboard

KeyBehavior
Enter, SpaceOpen the listbox; select the highlighted option when open
Arrow Down, Arrow UpOpen the listbox; move the highlight when open
Home, EndJump to the first or last option. Only while open
EscapeClose without changing the selection
TabClose and move on, keeping the current selection

There is no type-to-select. Typing a letter does nothing — Select handles only the keys above, and filtering by text is Combobox’s job.

Focus model

Focus stays on the activator the whole time. The highlighted option is tracked with aria-activedescendant rather than by moving DOM focus into the list — a virtual cursor. That is what keeps Escape and Tab predictable, and it means the reader’s focus is never stranded inside a list that has closed underneath them.

Naming

label produces the field’s accessible name. Without it the activator has only its own content to fall back on, which is the placeholder text while nothing is selected — a control announced as “Choose a region” that later announces as “Europe” has no stable name. Always pass label.

Options are named by their text content. When an option’s meaning lives in an icon, give it text too; the icon should stay decorative.

The popover

EmSelectContent renders in the top layer through the native popover API. That is a real accessibility benefit rather than only a layout one — the list is never clipped by an ancestor’s overflow and never trapped behind an unrelated stacking context, both of which produce menus a reader can hear but not see.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/