Skip to content

Number Field

A text input that holds a number, with stepping, bounds and locale formatting.

View source View as Markdown

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
App.vue
<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. Read v-model, never the input’s own value.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-label all the same, since a touch screen reader still lands on them.must
  • Make sure the field has a label, through a Label inside a Field or an aria-label on NumberField.Input.must
  • NumberField.ScrubArea asks for a pointer lock and carries on without one, so it is safe to render wherever the API is missing.may