Select & multi-select

One element, two leaf shapes: a scalar for <select>, an array for <select multiple>. Pick the mode in the schema; the directive follows.

Category
Directive binding
Element
<select> · <select multiple>
Modifiers
.number
Leaf types
scalar (single) · readonly Value[] (multiple)

Pick a country from the single select to watch the JSON readout switch its scalar value. In the multi-select below, hold ⌘ (or Ctrl) and click multiple options. Every picked option's value= attribute lands in the tags array in selection order. The directive reads the schema leaf at each path and infers single vs. multi automatically.

{
  "country": "us",
  "tags": []
}

Single select → scalar

When the schema leaf is a scalar (enum, string, number), <select> binds to that one value:

<select v-register="form.register('country')">
  <option value="us">United States</option>
  <option value="uk">United Kingdom</option>
</select>

The picked option's value= attribute lands in form.values.country. The schema's leaf type drives the storage type: z.enum(['us', 'uk']) keeps the value as the matching literal.

Multi-select → array

When the schema leaf is an array, <select multiple> writes every picked option's value into that array, in selection order:

<select v-register="form.register('tags')" multiple>
  <option value="design">Design</option>
  <option value="eng">Engineering</option>
  <option value="ops">Ops</option>
  <option value="sales">Sales</option>
</select>
const form = useForm({
  schema: z.object({
    tags: z.array(z.enum(['design', 'eng', 'ops', 'sales'])),
  }),
  defaultValues: { tags: [] },
})

form.values.tags // ['design', 'ops']

Deselecting an option removes it from the array; the array shape always reflects the current visual selection. No event-listener wiring on your side; the directive infers multi-mode from the multiple attribute and the schema's array leaf.

The .number modifier

<option value="..."> only stores strings. When the schema's array (or scalar) leaf is z.number() or z.array(z.number()), the .number modifier coerces every picked option's value to a number before the write:

<select v-register.number="form.register('priority')">
  <option value="1">Low</option>
  <option value="2">Medium</option>
  <option value="3">High</option>
</select>

The directive parses '1'1 per option. The Schema-driven coercion page documents the full leaf-type mapping.

Custom display values

The dropdown's option labels are pure HTML; bind them however you'd render any other list:

<script setup lang="ts">
  const countries = [
    { code: 'us', flag: '🇺🇸', name: 'United States' },
    { code: 'uk', flag: '🇬🇧', name: 'United Kingdom' },
    { code: 'ca', flag: '🇨🇦', name: 'Canada' },
    { code: 'au', flag: '🇦🇺', name: 'Australia' },
  ]
</script>

<template>
  <select v-register="form.register('country')">
    <option v-for="opt in countries" :key="opt.code" :value="opt.code">
      {{ opt.flag }} {{ opt.name }}
    </option>
  </select>
</template>

The directive only cares about each option's value= attribute; the visible label can be anything. <optgroup> works the same way: the directive reads option values whether or not they're nested under a group label.

Where to next