Number Field
A text input that holds a number, with stepping, bounds and locale formatting.
Number field holds a single number and renders it through Intl.NumberFormat.
The arrow keys step the value, a held stepper repeats, and every change is
clamped to the bounds. Useful when the value is a quantity somebody will nudge
as often as type; where the bounds matter more than the exact figure, use
Slider instead.
Quantity
Budget
<template>
<div class="flex w-[17rem] flex-col gap-6">
<div class="flex flex-col gap-1.5">
<span :class="caption">Quantity</span>
<NumberField.Root
v-model="quantity"
class="group"
:options="{ min: 0, max: 99, name: 'quantity' }"
>
<NumberField.Group :class="box">
<NumberField.Decrement :class="stepper" aria-label="One fewer">
−
</NumberField.Decrement>
<NumberField.Input :class="control" />
<NumberField.Increment :class="stepper" aria-label="One more">
+
</NumberField.Increment>
</NumberField.Group>
</NumberField.Root>
</div>
<div class="flex flex-col gap-1.5">
<span :class="caption">Budget</span>
<NumberField.Root
v-model="budget"
class="group"
:options="{
min: 0,
step: 50,
name: 'budget',
locale: 'de-DE',
format: { style: 'currency', currency: 'EUR' },
}"
>
<NumberField.Group :class="box">
<NumberField.Decrement :class="stepper" aria-label="Fifty less">
−
</NumberField.Decrement>
<NumberField.Input :class="control" />
<NumberField.Increment :class="stepper" aria-label="Fifty more">
+
</NumberField.Increment>
</NumberField.Group>
</NumberField.Root>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NumberField } from '@maas/mirror/vue/NumberField'
const quantity = ref(1)
const budget = ref(1250)
const caption = 'type-component-2xs text-surface-muted'
const box = [
'relative isolate flex h-12 w-full items-center justify-between',
'rounded-component-lg border-surface border-2',
'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
'outline-4 outline-transparent focus-within:focus-ring',
'group-data-[disabled=true]:border-disabled-subtle',
].join(' ')
const stepper = [
'flex h-full w-11 shrink-0 cursor-pointer items-center justify-center',
'type-component-lg leading-none text-surface-muted select-none',
'rounded-component-md bg-transparent outline-none',
'hover:text-surface active:text-primary-solid',
'data-[disabled=true]:text-disabled-muted data-[disabled=true]:cursor-not-allowed',
].join(' ')
const control = [
'type-component-lg leading-[normal]! text-surface tabular-nums',
'h-full w-full min-w-0 appearance-none bg-transparent text-center outline-none',
'data-[disabled=true]:text-disabled-solid',
].join(' ')
</script>
Usage guidelines
- The value is a number or
null, and the input shows the formatted text for it. Readv-model, never the input’s ownvalue.must - The steppers sit outside the tab order on purpose, because the input already reaches the value from the keyboard. Give each one an
aria-labelall the same, since a touch screen reader still lands on them.must - Make sure the field has a label, through a
Labelinside aFieldor anaria-labelonNumberField.Input.must NumberField.ScrubAreaasks for a pointer lock and carries on without one, so it is safe to render wherever the API is missing.may