Number Spinner
A numeric input with increment and decrement controls
Basic Usage
Simple number spinner with min/max constraints.
Simple Number Spinner
Current value: 0
With Constraints
Quantity: 1
1
2<script lang="ts">
3 import { NumberSpinner, Badge } from "@kareyes/aether";
4
5 let basicValue = $state(0);
6 let quantityValue = $state(1);
7</script>
8
9<div class="grid gap-8 md:grid-cols-2">
10 <div class="space-y-2">
11 <h3 class="text-lg font-medium">Simple Number Spinner</h3>
12 <NumberSpinner bind:value={basicValue} min={0} max={100} />
13 <p class="text-sm text-muted-foreground">
14 Current value: <Badge variant="secondary">{basicValue}</Badge>
15 </p>
16 </div>
17 <div class="space-y-2">
18 <h3 class="text-lg font-medium">With Constraints</h3>
19 <NumberSpinner bind:value={quantityValue} min={1} max={99} />
20 <p class="text-sm text-muted-foreground">
21 Quantity: <Badge variant="outline">{quantityValue}</Badge>
22 </p>
23 </div>
24</div>Variants
Available visual styles for the number spinner.
Default
Outline
Filled
Ghost
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4</script>
5
6<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
7 <NumberSpinner variant="default" value={10} />
8 <NumberSpinner variant="outline" value={10} />
9 <NumberSpinner variant="filled" value={10} />
10 <NumberSpinner variant="ghost" value={10} />
11</div>Sizes
Different size options for the number spinner.
Small
Default
Large
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4</script>
5
6<div class="grid gap-8 md:grid-cols-3">
7 <NumberSpinner size="sm" value={5} min={0} max={10} />
8 <NumberSpinner size="default" value={5} min={0} max={10} />
9 <NumberSpinner size="lg" value={5} min={0} max={10} />
10</div>Orientations
Vertical and horizontal button placement options.
Vertical (Default)
Buttons are positioned vertically on the right
Horizontal
Buttons are positioned on both sides
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4</script>
5
6<div class="grid gap-8 md:grid-cols-2">
7 <div class="space-y-2">
8 <h3 class="text-lg font-medium">Vertical (Default)</h3>
9 <NumberSpinner orientation="vertical" value={10} min={0} max={20} />
10 </div>
11 <div class="space-y-2">
12 <h3 class="text-lg font-medium">Horizontal</h3>
13 <NumberSpinner orientation="horizontal" value={10} min={0} max={20} />
14 </div>
15</div>Advanced Features
Decimal precision, step control, and custom configurations.
Price Input (Decimal Precision)
Price: $9.99
Temperature (Step Control)
Temperature: 20.5°C
Large Step Size
Increments by 10
Integer Only
No decimal values allowed
1
2<script lang="ts">
3 import { NumberSpinner, Badge } from "@kareyes/aether";
4
5 let priceValue = $state(9.99);
6 let temperatureValue = $state(20.5);
7</script>
8
9<div class="grid gap-8 md:grid-cols-2">
10 <!-- Price Input (Decimal Precision) -->
11 <NumberSpinner
12 bind:value={priceValue}
13 min={0} max={9999.99}
14 step={0.01} precision={2}
15 placeholder="0.00" variant="outline"
16 />
17
18 <!-- Temperature (Step Control) -->
19 <NumberSpinner
20 bind:value={temperatureValue}
21 min={-50} max={50}
22 step={0.5} precision={1}
23 orientation="horizontal" size="lg"
24 />
25
26 <!-- Large Step Size -->
27 <NumberSpinner value={50} min={0} max={1000} step={10} />
28
29 <!-- Integer Only -->
30 <NumberSpinner value={25} min={0} max={100} step={1} />
31</div>Practical Examples
Real-world usage patterns like shopping carts.
Shopping Cart
Total Items: 4
1
2<script lang="ts">
3 import { NumberSpinner, Badge } from "@kareyes/aether";
4
5 let cartQuantity1 = $state(1);
6 let cartQuantity2 = $state(2);
7 let cartQuantity3 = $state(1);
8 const totalCartItems = $derived((cartQuantity1 ?? 0) + (cartQuantity2 ?? 0) + (cartQuantity3 ?? 0));
9</script>
10
11<div class="space-y-3 rounded-lg border p-4">
12 <div class="flex items-center justify-between">
13 <span class="text-sm">Product A - $19.99</span>
14 <NumberSpinner bind:value={cartQuantity1} min={1} max={99} size="sm" orientation="horizontal" />
15 </div>
16 <div class="flex items-center justify-between">
17 <span class="text-sm">Product B - $29.99</span>
18 <NumberSpinner bind:value={cartQuantity2} min={1} max={99} size="sm" orientation="horizontal" />
19 </div>
20 <div class="flex items-center justify-between">
21 <span class="text-sm">Product C - $9.99</span>
22 <NumberSpinner bind:value={cartQuantity3} min={1} max={99} size="sm" orientation="horizontal" />
23 </div>
24 <div class="border-t pt-3">
25 <p class="text-sm font-medium">
26 Total Items: <Badge variant="secondary">{totalCartItems}</Badge>
27 </p>
28 </div>
29</div>Error States
Validation error display with the number spinner.
With Error
Age is required
Valid State
No errors
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4
5 let ageValue = $state(null);
6</script>
7
8<div class="grid gap-8 md:grid-cols-2">
9 <div class="space-y-2">
10 <NumberSpinner bind:value={ageValue} error={!ageValue} required min={1} max={150} />
11 {#if !ageValue}
12 <p class="text-sm text-destructive">Age is required</p>
13 {/if}
14 </div>
15 <div class="space-y-2">
16 <NumberSpinner value={25} error={false} />
17 <p class="text-sm text-muted-foreground">No errors</p>
18 </div>
19</div>With Field Component
Using the Field component for labels, descriptions, and error handling.
Number of items to order
Enter the price in USD (up to $9,999.99)
Set your preferred temperature (-10°C to 40°C)
1
2<script lang="ts">
3 import { NumberSpinner, Field } from "@kareyes/aether";
4
5 let ageValue = $state(null);
6</script>
7
8<div class="max-w-2xl space-y-6">
9 <Field label="Age" description="Enter your age (1-150)" error={!ageValue ? "Age is required" : undefined} required>
10 <NumberSpinner bind:value={ageValue} error={!ageValue} min={1} max={150} />
11 </Field>
12
13 <Field label="Quantity" description="Number of items to order" required>
14 <NumberSpinner value={1} min={1} max={100} orientation="horizontal" />
15 </Field>
16
17 <Field label="Product Price" description="Enter the price in USD (up to $9,999.99)" required>
18 <NumberSpinner value={0} min={0} max={9999.99} step={0.01} precision={2} variant="filled" placeholder="0.00" />
19 </Field>
20
21 <Field label="Room Temperature" description="Set your preferred temperature (-10°C to 40°C)">
22 <NumberSpinner value={22} min={-10} max={40} step={0.5} precision={1} orientation="horizontal" size="lg" />
23 </Field>
24</div>Form Integration
Complete form example with validation and submission.
1
2<script lang="ts">
3 import { NumberSpinner, Field, Badge } from "@kareyes/aether";
4
5 let formQuantity = $state(1);
6 let formPrice = $state(0);
7 let formAge = $state(null);
8 const formValid = $derived(formQuantity > 0 && formPrice > 0 && formAge !== null && formAge > 0);
9</script>
10
11<form class="max-w-2xl space-y-6 rounded-lg border p-6" onsubmit={(e) => { e.preventDefault(); }}>
12 <Field label="Quantity" error={formQuantity < 1 ? "Quantity must be at least 1" : undefined} required>
13 <NumberSpinner bind:value={formQuantity} error={formQuantity < 1} min={1} max={100} />
14 </Field>
15
16 <Field label="Unit Price" error={formPrice <= 0 ? "Price must be greater than 0" : undefined} required>
17 <NumberSpinner bind:value={formPrice} error={formPrice <= 0} min={0} max={9999.99} step={0.01} precision={2} variant="outline" />
18 </Field>
19
20 <Field label="Customer Age" error={!formAge || formAge < 18 ? "Must be 18 or older" : undefined} required>
21 <NumberSpinner bind:value={formAge} error={!formAge || formAge < 18} min={18} max={150} />
22 </Field>
23
24 <div class="flex items-center justify-between pt-4">
25 <p class="text-sm text-muted-foreground">
26 Total: <Badge variant="secondary">${((formPrice ?? 0) * (formQuantity ?? 0)).toFixed(2)}</Badge>
27 </p>
28 <button type="submit" class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" disabled={!formValid}>
29 Submit Order
30 </button>
31 </div>
32</form>Disabled State
Number spinner in disabled state for both orientations.
Vertical Disabled
Horizontal Disabled
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4</script>
5
6<div class="grid gap-8 md:grid-cols-2">
7 <NumberSpinner value={10} disabled={true} />
8 <NumberSpinner value={10} disabled={true} orientation="horizontal" />
9</div>All Variants & Sizes
Complete matrix of all variant and size combinations.
Vertical Orientation
Horizontal Orientation
1
2<script lang="ts">
3 import { NumberSpinner } from "@kareyes/aether";
4</script>
5
6<div class="grid gap-8 lg:grid-cols-2">
7 <!-- Vertical -->
8 <div class="space-y-4">
9 <h3 class="font-semibold">Vertical Orientation</h3>
10 {#each ["default", "outline", "filled", "ghost"] as variant}
11 <div class="grid grid-cols-3 gap-4">
12 <NumberSpinner variant={variant} value={10} size="sm" />
13 <NumberSpinner variant={variant} value={10} size="default" />
14 <NumberSpinner variant={variant} value={10} size="lg" />
15 </div>
16 {/each}
17 </div>
18
19 <!-- Horizontal -->
20 <div class="space-y-4">
21 <h3 class="font-semibold">Horizontal Orientation</h3>
22 {#each ["default", "outline", "filled", "ghost"] as variant}
23 <div class="grid grid-cols-3 gap-4">
24 <NumberSpinner variant={variant} value={10} size="sm" orientation="horizontal" />
25 <NumberSpinner variant={variant} value={10} size="default" orientation="horizontal" />
26 <NumberSpinner variant={variant} value={10} size="lg" orientation="horizontal" />
27 </div>
28 {/each}
29 </div>
30</div>Features
- ✅ Multiple Layouts: Vertical (default) and horizontal orientations
- ✅ Variants: Multiple visual styles (default, outline, filled, ghost)
- ✅ Sizes: Three size options (sm, default, lg)
- ✅ Value Constraints: Min/max value limits with automatic clamping
- ✅ Step Control: Configurable increment/decrement step size
- ✅ Precision: Control decimal places for floating-point values
- ✅ Keyboard Support: Arrow keys for increment/decrement
- ✅ Error Handling: Built-in error state with visual feedback
- ✅ Field Integration: Works seamlessly with Field component
- ✅ Accessibility: Full keyboard navigation and ARIA support
- ✅ TypeScript: Complete type safety
Simple Number Spinner
<script>
import { NumberSpinner } from "@kareyes/aether";
let quantity = $state(1);
</script>
<NumberSpinner
bind:value={quantity}
min={0}
max={100}
/>
With Constraints
<script>
let price = $state(9.99);
</script>
<NumberSpinner
bind:value={price}
min={0}
max={999.99}
step={0.01}
precision={2}
placeholder="0.00"
/>
Horizontal Layout
<NumberSpinner
bind:value={quantity}
orientation="horizontal"
min={0}
max={10}
/>
Variants
Default
<NumberSpinner
variant="default"
bind:value={count}
/>
Outline
<NumberSpinner
variant="outline"
bind:value={count}
/>
Filled
<NumberSpinner
variant="filled"
bind:value={count}
/>
Ghost
<NumberSpinner
variant="ghost"
bind:value={count}
/>
Sizes
<!-- Small -->
<NumberSpinner
size="sm"
bind:value={count}
/>
<!-- Default -->
<NumberSpinner
size="default"
bind:value={count}
/>
<!-- Large -->
<NumberSpinner
size="lg"
bind:value={count}
/>
Orientations
Vertical (Default)
<NumberSpinner
orientation="vertical"
bind:value={quantity}
/>
Horizontal
<NumberSpinner
orientation="horizontal"
bind:value={quantity}
/>
Advanced Features
Precision Control
Control the number of decimal places:
<script>
let temperature = $state(20.5);
</script>
<NumberSpinner
bind:value={temperature}
step={0.1}
precision={1}
min={-50}
max={50}
/>
Large Step Sizes
<NumberSpinner
bind:value={count}
step={10}
min={0}
max={1000}
/>
Error States
<script>
let quantity = $state(null);
let hasError = $derived(!quantity || quantity < 1);
</script>
<NumberSpinner
bind:value={quantity}
error={hasError}
required
min={1}
/>
With Field Component
<script>
import { Field } from "@kareyes/aether";
let quantity = $state(1);
</script>
<Field
label="Quantity"
description="Enter the number of items"
error={quantity < 1 ? "Quantity must be at least 1" : undefined}
required
>
<NumberSpinner
bind:value={quantity}
error={quantity < 1}
min={1}
max={999}
/>
</Field>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
number | null |
null |
Current value (bindable) |
variant |
"default" | "outline" | "filled" | "ghost" |
"default" |
Visual variant |
size |
"sm" | "default" | "lg" |
"default" |
Size variant |
orientation |
"vertical" | "horizontal" |
"vertical" |
Button layout orientation |
min |
number |
undefined |
Minimum allowed value |
max |
number |
undefined |
Maximum allowed value |
step |
number |
1 |
Increment/decrement step size |
precision |
number |
undefined |
Number of decimal places |
disabled |
boolean |
false |
Disable the input |
required |
boolean |
false |
Mark as required field |
error |
boolean |
false |
Error state with visual feedback |
placeholder |
string |
undefined |
Placeholder text |
class |
string |
undefined |
Additional CSS classes for root |
inputClass |
string |
undefined |
Additional CSS classes for input |
ref |
HTMLInputElement | null |
null |
Reference to input element (bindable) |
onValueChange |
(value: number | null) => void |
undefined |
Callback when value changes |
onError |
(error: boolean) => void |
undefined |
Callback when error state changes |
Keyboard Support
| Key | Action |
|---|---|
Arrow Up |
Increment value by step |
Arrow Down |
Decrement value by step |
Tab |
Move focus to/from the input |
Examples
Shopping Cart Quantity
<script>
let cartItems = $state([
{ id: 1, name: "Product A", quantity: 1 },
{ id: 2, name: "Product B", quantity: 2 },
]);
</script>
{#each cartItems as item}
<div class="flex items-center gap-4">
<span>{item.name}</span>
<NumberSpinner
bind:value={item.quantity}
min={1}
max={99}
size="sm"
/>
</div>
{/each}
Price Input
<script>
let price = $state(0);
</script>
<NumberSpinner
bind:value={price}
min={0}
max={9999.99}
step={0.01}
precision={2}
placeholder="0.00"
variant="outline"
onValueChange={(val) => console.log('Price:', val)}
/>
Temperature Control
<script>
let temperature = $state(20);
</script>
<NumberSpinner
bind:value={temperature}
min={-50}
max={50}
step={0.5}
precision={1}
orientation="horizontal"
size="lg"
/>
Form Integration
<script>
import { Field } from "@kareyes/aether";
let age = $state(null);
let quantity = $state(1);
let price = $state(0);
function handleSubmit() {
console.log({ age, quantity, price });
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<div class="space-y-4">
<Field
label="Age"
description="Enter your age"
required
>
<NumberSpinner
bind:value={age}
min={1}
max={150}
/>
</Field>
<Field
label="Quantity"
description="Number of items"
required
>
<NumberSpinner
bind:value={quantity}
min={1}
max={100}
orientation="horizontal"
/>
</Field>
<Field
label="Price"
description="Item price in USD"
required
>
<NumberSpinner
bind:value={price}
min={0}
max={9999.99}
step={0.01}
precision={2}
variant="filled"
/>
</Field>
<button type="submit">Submit</button>
</div>
</form>
Accessibility
- ✅ Keyboard Navigation: Full support for arrow keys and tab navigation
- ✅ ARIA Labels: Increment/decrement buttons have descriptive labels
- ✅ Screen Readers: Proper announcements for value changes
- ✅ Focus Management: Clear visual focus indicators
- ✅ Error States: Proper aria-invalid attributes
ARIA Attributes
aria-label- Applied to increment/decrement buttonsaria-invalid- Indicates error statetabindex="-1"- Buttons are not in tab order (input handles keyboard)
Best Practices
- Set Constraints: Always define
minandmaxwhen appropriate - Use Precision: Specify
precisionfor decimal values to avoid floating-point errors - Appropriate Steps: Choose step sizes that make sense for your use case
- Validation: Combine with Field component for proper error messaging
- Accessibility: Ensure proper labels and error messages
- Null Handling: Handle
nullvalues appropriately in your logic
Common Patterns
Quantity Selector
Use vertical orientation with small size for compact quantity controls.
Price Input
Use precision and appropriate step for currency values.
Temperature/Measurement
Use horizontal orientation with decimal precision for scientific values.
Age/Year Input
Use integer values with reasonable min/max constraints.
Browser Support
Works in all modern browsers with full accessibility support.
Using NumberSpinner with Field Component
The Field component provides a consistent way to add labels, descriptions, and error handling to your NumberSpinner components.
Basic Field Usage
<script>
import { NumberSpinner } from "@kareyes/aether";
import { Field } from "@kareyes/aether";
let quantity = $state(1);
</script>
<Field
label="Quantity"
description="Select the number of items"
>
<NumberSpinner
bind:value={quantity}
min={1}
max={100}
/>
</Field>
With Validation
<script>
import { NumberSpinner } from "@kareyes/aether";
import { Field } from "@kareyes/aether";
let age = $state(null);
let error = $derived(age === null || age < 18);
</script>
<Field
label="Age"
description="You must be 18 or older"
required
error={error ? "You must be at least 18 years old" : undefined}
>
<NumberSpinner
bind:value={age}
min={0}
max={120}
placeholder="Enter age"
error={error}
/>
</Field>
Price Input with Field
<script>
import { NumberSpinner } from "@kareyes/aether";
import { Field } from "@kareyes/aether";
let price = $state(9.99);
</script>
<Field
label="Product Price"
description="Set the price in USD"
required
>
<NumberSpinner
bind:value={price}
min={0}
max={9999.99}
step={0.01}
precision={2}
variant="outline"
size="lg"
/>
</Field>
Horizontal Layout with Field
<Field
label="Temperature"
description="Set temperature in Celsius"
>
<NumberSpinner
bind:value={temperature}
min={-50}
max={50}
step={0.5}
precision={1}
orientation="horizontal"
variant="filled"
/>
</Field>
Different Variants with Field
<Field
label="Stock Quantity"
description="Available inventory"
>
<NumberSpinner
variant="filled"
size="lg"
bind:value={stock}
min={0}
max={10000}
/>
</Field>
<Field
label="Discount Percentage"
description="Enter discount value"
>
<NumberSpinner
variant="ghost"
bind:value={discount}
min={0}
max={100}
step={5}
/>
</Field>
Complete Product Form
<script>
import { NumberSpinner, Button, FieldPrimitives , Field} from "@kareyes/aether";
let formData = $state({
price: 29.99,
quantity: 1,
discount: 0,
weight: 1.5,
});
let priceError = $derived(
formData.price === null || formData.price <= 0
? "Price must be greater than 0"
: undefined
);
let quantityError = $derived(
formData.quantity === null || formData.quantity < 1
? "Quantity must be at least 1"
: undefined
);
function handleSubmit() {
if (!priceError && !quantityError) {
console.log("Product data:", formData);
}
}
</script>
<div class="w-full max-w-md">
<FieldPrimitives.Set>
<FieldPrimitives.Legend>Product Details</FieldPrimitives.Legend>
<FieldPrimitives.Description>Configure product pricing and inventory</FieldPrimitives.Description>
<FieldPrimitives.Separator />
<FieldPrimitives.Group class="gap-4">
<Field
label="Price (USD)"
description="Set the product price"
required
error={priceError}
>
<NumberSpinner
bind:value={formData.price}
min={0}
max={9999.99}
step={0.01}
precision={2}
variant="outline"
size="lg"
error={!!priceError}
/>
</Field>
<Field
label="Quantity"
description="Available stock quantity"
required
error={quantityError}
>
<NumberSpinner
bind:value={formData.quantity}
min={1}
max={10000}
variant="filled"
error={!!quantityError}
/>
</Field>
<Field
label="Discount (%)"
description="Optional discount percentage"
>
<NumberSpinner
bind:value={formData.discount}
min={0}
max={100}
step={5}
orientation="horizontal"
/>
</Field>
<Field
label="Weight (kg)"
description="Product weight for shipping"
>
<NumberSpinner
bind:value={formData.weight}
min={0}
max={1000}
step={0.1}
precision={1}
variant="ghost"
/>
</Field>
</FieldPrimitives.Group>
<div class="flex gap-4 pt-4">
<Button
onclick={handleSubmit}
disabled={!!priceError || !!quantityError}
>
Save Product
</Button>
<Button variant="outline" type="button">
Cancel
</Button>
</div>
</FieldPrimitives.Set>
</div>