<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>
Usage
import { NumberInput } from '@hina-ui/vue'
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>
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>
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>
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>
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>
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>
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>
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.
<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>
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 isrole="spinbutton"witharia-valuenow,aria-valueminandaria-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
labelelement or anaria-labelfor its name.invalidalso setsaria-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 |