NumberInput

A field for entering numbers.

<script setup lang="ts">
  import { NumberInput } from '@hina-ui/vue'
</script>

<template>
  <NumberInput :default-value="3" :min="0" :max="10" aria-label="Quantity" class="w-40" />
</template>
vue

Usage

import { NumberInput } from '@hina-ui/vue'
ts

The field pairs a text area with a pair of stepper buttons, with v-model bound to the number. Arrow keys, Page Up and Page Down, Home and End, and the mouse wheel while focused all adjust the value. Attributes it does not declare land on the inner input.

<script setup lang="ts">
  import { ref } from 'vue'
  import { NumberInput } from '@hina-ui/vue'

  const quantity = ref(1)
</script>

<template>
  <NumberInput v-model="quantity" aria-label="Quantity" placeholder="Quantity" class="w-40" />
</template>
vue

Examples

Range and step

min and max bound the value and step sets how much each change moves it. Typed values snap to the step by default; turn stepSnapping off to only clamp the range.

<script setup lang="ts">
  import { ref } from 'vue'
  import { NumberInput, Stack } from '@hina-ui/vue'

  const rating = ref(2.5)
  const pages = ref(12)
</script>

<template>
  <Stack class="w-40">
    <NumberInput v-model="rating" :min="0" :max="10" :step="0.5" aria-label="Rating" />
    <NumberInput v-model="pages" :min="1" :step="5" :step-snapping="false" aria-label="Pages" />
  </Stack>
</template>
vue

Format

formatOptions takes Intl.NumberFormat options, so currencies, percentages and units all display. locale sets the separators and symbols; it defaults to the locale pack’s language.

<script setup lang="ts">
  import { ref } from 'vue'
  import { NumberInput, Stack } from '@hina-ui/vue'

  const price = ref(1280)
  const discount = ref(0.35)
  const euro = ref(1234.5)
</script>

<template>
  <Stack class="w-48">
    <NumberInput
      v-model="price"
      :format-options="{ style: 'currency', currency: 'CNY' }"
      aria-label="Price"
    />
    <NumberInput
      v-model="discount"
      :step="0.01"
      :min="0"
      :max="1"
      :format-options="{ style: 'percent' }"
      aria-label="Discount"
    />
    <NumberInput
      v-model="euro"
      locale="de-DE"
      :format-options="{ style: 'currency', currency: 'EUR' }"
      aria-label="Price in euros"
    />
  </Stack>
</template>
vue

Sizes

Three sizes, matching the input.

<script setup lang="ts">
  import { NumberInput, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-40">
    <NumberInput size="sm" :default-value="1" aria-label="Small" />
    <NumberInput size="md" :default-value="1" aria-label="Medium" />
    <NumberInput size="lg" :default-value="1" aria-label="Large" />
  </Stack>
</template>
vue

Variants

primary has a border, background and shadow; secondary uses a tinted background; bare is transparent, without a border, shadow, hover fill or container focus ring. Size and padding keep their existing settings.

bare preserves the disabled state and aria-invalid; FormField can display the error message. Dividers between the steppers and the text area, and between the two steppers, are also hidden. The buttons remain interactive.

<script setup lang="ts">
  import { NumberInput, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-xs">
    <NumberInput variant="primary" aria-label="primary" :default-value="1" />
    <NumberInput variant="secondary" aria-label="secondary" :default-value="1" />
    <NumberInput variant="bare" aria-label="bare" :default-value="1" />
  </Stack>
</template>
vue

States

invalid marks a failed validation, disabled prevents editing, and readonly allows focus and copying but no changes.

<script setup lang="ts">
  import { NumberInput, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-40">
    <NumberInput invalid :default-value="120" :max="99" aria-label="Invalid" />
    <NumberInput disabled :default-value="3" aria-label="Disabled" />
    <NumberInput readonly :default-value="3" aria-label="Read-only" />
  </Stack>
</template>
vue

Without steppers

With controls off only the text area remains; keyboard and wheel still work.

<script setup lang="ts">
  import { NumberInput } from '@hina-ui/vue'
</script>

<template>
  <NumberInput :controls="false" :min="1" placeholder="Page" aria-label="Page" class="w-40" />
</template>
vue

In a form

Inside a FormField the label points at the input, and the description and error message are rendered by the field; validation rules and submission belong to the Form. The value is a number or null, so range rules go on the number.

1 to 9999

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Form, FormField, NumberInput, Text } from '@hina-ui/vue'

  const schema = v.object({
    price: v.pipe(
      v.number('Enter a price'),
      v.minValue(1, 'The price must be at least 1'),
      v.maxValue(9999, 'The price must be at most 9999'),
    ),
  })

  const values = reactive({ price: null as number | null })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="price" label="Price" description="1 to 9999" required>
      <NumberInput v-model="values.price" :min="0" :step="1" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">List</Button>
    <Text v-if="saved" tone="muted" size="sm">Listed: {{ saved }}</Text>
  </Form>
</template>
vue

Behaviour

  • The text is parsed on blur or Enter. Unparseable text reverts to the previous value, and out-of-range values clamp to the bounds.
  • Characters that cannot form a number are rejected while typing.
  • The steppers repeat while held and disable at the bound they have reached.
  • Hover, focus, invalid and disabled behave the same as Input.

Accessibility

  • The root is role="group" and the text area is role="spinbutton" with aria-valuenow, aria-valuemin and aria-valuemax.
  • The steppers are outside the Tab order; keyboard users adjust with the arrow keys. Their names come from the locale pack.
  • Pair it with a label element or an aria-label for its name. invalid also sets aria-invalid.

API

Props

Prop
Type
Default
Description
modelValue
number | null
—
The value
defaultValue
number
—
Initial value when uncontrolled
min
number
—
Minimum
max
number
—
Maximum
step
number
1
Step
stepSnapping
boolean
true
Whether values snap to the step
formatOptions
Intl.NumberFormatOptions
—
Display format
locale
string
—
Locale used for formatting
controls
boolean
true
Whether the steppers are shown
variant
'primary' | 'secondary' | 'bare'
'primary'
Variant
size
'sm' | 'md' | 'lg'
'md'
Size
invalid
boolean
false
Whether validation failed
disabled
boolean
false
Whether the field is disabled
readonly
boolean
false
Whether the field is read-only
class
string
—
Classes appended to the root element
Event
Payload
Description
update:modelValue
value: number | undefined
The value changed