Spin Button
Edit a numeric draft directly or change it with increment controls.
Usage
Integer
Accept whole-number drafts and expose increment and decrement controls.
Allowed range: 0–20 · Step 1
<!-- Spin Button / Integer -->
<script setup lang="ts">
import { ref } from 'vue'
import { SpinButtonDecrement, SpinButtonIncrement, SpinButtonInput, SpinButtonRoot } from '@sectile/vue/spin-button'
const value = ref('4')
</script>
<template>
<SpinButtonRoot v-model="value" min="0" max="20" step="1">
<SpinButtonDecrement aria-label="Decrease">−</SpinButtonDecrement>
<SpinButtonInput />
<SpinButtonIncrement aria-label="Increase">+</SpinButtonIncrement>
</SpinButtonRoot>
</template>Controlled
Let the parent own the current value and apply accepted changes back to the component.
Allowed range: -10–10 · Step 0.5
<!-- Spin Button / Controlled -->
<script setup lang="ts">
import { ref } from 'vue'
import { SpinButtonDecrement, SpinButtonIncrement, SpinButtonInput, SpinButtonRoot } from '@sectile/vue/spin-button'
const value = ref('1.5')
</script>
<template>
<SpinButtonRoot v-model="value" min="-10" max="10" step="0.5">
<SpinButtonDecrement aria-label="Decrease">−</SpinButtonDecrement>
<SpinButtonInput />
<SpinButtonIncrement aria-label="Increase">+</SpinButtonIncrement>
</SpinButtonRoot>
</template>Examples
Input recovery
Type a quantity directly; leaving an invalid edit restores the last accepted value.
Allowed range: 0–20 · Step 1
<!-- Spin Button / Invalid Draft -->
<script setup lang="ts">
import { ref } from 'vue'
import { SpinButtonDecrement, SpinButtonIncrement, SpinButtonInput, SpinButtonRoot } from '@sectile/vue/spin-button'
const value = ref('10')
const draft = ref<string | null>(null)
</script>
<template>
<SpinButtonRoot v-model="value" min="0" max="20" step="1" @update:draft="draft = $event">
<SpinButtonDecrement aria-label="Decrease">−</SpinButtonDecrement>
<SpinButtonInput />
<SpinButtonIncrement aria-label="Increase">+</SpinButtonIncrement>
</SpinButtonRoot>
<p v-if="draft !== null">Leaving an invalid edit restores {{ value }}.</p>
</template>API
Vue package: @sectile/vue/spin-button
SpinButtonRootSpinButtonInputSpinButtonIncrementSpinButtonDecrement
Props
SpinButtonRootProps
as- Type
PrimitiveAsDefault'div'Element or component rendered for this part.
asChild- Type
booleanDefaultfalseWhether to merge this part into its single child instead of rendering a wrapper.
defaultDraft- Type
string | nullDefaultnullEditable draft used for uncontrolled initial state.
defaultValue- Type
number | stringDefaultundefinedInitial value used when the component owns its state.
disabled- Type
booleanDefaultfalseWhether interaction is unavailable.
draft- Type
string | nullDefaultundefinedControlled editable draft before commit.
label- Type
stringDefaultundefinedAccessible name announced for the control.
max- Type
number | stringDefaultRequiredLargest value accepted by the component.
min- Type
number | stringDefaultRequiredSmallest value accepted by the component.
modelValue- Type
number | stringDefaultundefinedCurrent value when state is controlled by the parent.
pageStep- Type
numberDefault10Larger increment used by Page Up and Page Down.
policies- Type
SpinButtonOptions['policies']DefaultundefinedBehavior policies that customize validation, movement, or selection.
readonly- Type
booleanDefaultfalseWhether the value can be inspected but not changed.
step- Type
number | stringDefault1Smallest value increment accepted by the component.
SpinButtonInputProps
as- Type
PrimitiveAsDefault'input'Element or component rendered for this part.
asChild- Type
booleanDefaultfalseWhether to merge this part into its single child instead of rendering a wrapper.
form- Type
stringDefaultundefinedID of the native form associated with the control.
name- Type
stringDefaultundefinedName used for native form submission.
required- Type
booleanDefaultfalseWhether the control must contain a valid value before submission.
SpinButtonTriggerProps
as- Type
PrimitiveAsDefaultundefinedElement or component rendered for this part.
asChild- Type
booleanDefaultundefinedWhether to merge this part into its single child instead of rendering a wrapper.
Slots
SpinButtonSlotProps
disabled- Type
booleanWhether interaction is unavailable.
draft- Type
string | nullCurrent uncommitted input text.
readonly- Type
booleanWhether the value can be inspected but not changed.
text- Type
stringFormatted text for the current value.
value- Type
stringCurrent value exposed by this contract.
Events
SpinButtonRoot
update:draft- Payload
string | nullEmitted when the editable draft changes.
update:modelValue- Payload
stringEmitted when the component requests a new controlled value.
Other types
SpinButtonValueChangeHandler
type SpinButtonValueChangeHandler = (value: string) => voidSpinButtonDraftChangeHandler
type SpinButtonDraftChangeHandler = (value: string | null) => voidParts
Shared scope: [data-scope="spin-button"]. Combine it with a part selector to keep styles local to this component.
| Part | Selector | Role | Extra attributes |
|---|---|---|---|
root | [data-part="root"] | Defines the component boundary and owns its composed parts. | — |
input | [data-part="input"] | Accepts the editable value or draft. | — |
increment | [data-part="increment"] | Increases the value by one configured step. | — |
decrement | [data-part="decrement"] | Decreases the value by one configured step. | — |
Keyboard interaction
| Key | Behavior |
|---|---|
| Arrow Right / Arrow Up | Increase the value by one step. |
| Arrow Left / Arrow Down | Decrease the value by one step. |
| Home / End | Move to the minimum or maximum value. |
| Page Up / Page Down | Change the value by the configured page step when supported. |
Accessibility
The input exposes spinbutton value metadata while increment and decrement remain named native controls.
See the corresponding WAI-ARIA pattern for the host accessibility contract.
