ComponentsNumberInput
NumberInput
Collect a number with optional minimum, maximum, and step values.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import { NumberInput } from "@dethink/components";Try the examples, then open the code to use them in your app.
Bounded quantity
type=number gives native min/max/step behavior — arrow keys and spinners step within bounds.
Between 1 and 500 seats.
Show sourceexamples/number-input/basic.tsx
"use client";
import {
Field,
FieldControl,
FieldDescription,
FieldLabel,
NumberInput,
} from "@dethink/components";
export function NumberInputBasic() {
return (
<div className="mx-auto max-w-xs">
<Field id="ni-seats">
<FieldLabel>Seats</FieldLabel>
<FieldControl asChild>
<NumberInput
name="seats"
type="number"
min={1}
max={500}
step={1}
defaultValue={5}
/>
</FieldControl>
<FieldDescription>Between 1 and 500 seats.</FieldDescription>
</Field>
</div>
);
}Keypad modes and states
numberMode switches the mobile keypad between decimal and digits-only; disabled and invalid states flow through Field.
Show sourceexamples/number-input/modes.tsx
"use client";
import {
Field,
FieldControl,
FieldLabel,
NumberInput,
} from "@dethink/components";
export function NumberInputModes() {
return (
<div className="mx-auto grid max-w-md gap-5 sm:grid-cols-2">
<Field id="ni-amount">
<FieldLabel>Amount (decimal keypad)</FieldLabel>
<FieldControl asChild>
<NumberInput numberMode="decimal" placeholder="19.99" />
</FieldControl>
</Field>
<Field id="ni-code">
<FieldLabel>Verification code (numeric keypad)</FieldLabel>
<FieldControl asChild>
<NumberInput
numberMode="numeric"
placeholder="123456"
maxLength={6}
/>
</FieldControl>
</Field>
<Field id="ni-disabled" disabled>
<FieldLabel>Disabled</FieldLabel>
<FieldControl asChild>
<NumberInput type="number" defaultValue={42} />
</FieldControl>
</Field>
<Field id="ni-invalid" invalid>
<FieldLabel>Invalid</FieldLabel>
<FieldControl asChild>
<NumberInput type="number" defaultValue={-3} min={0} />
</FieldControl>
</Field>
</div>
);
}Examples that combine components for common tasks.
Budget allocator
The 100% invariant lives across all three inputs, so every field flips invalid together, a stacked bar visualizes the split, and a live region explains the group-level error.
Budget fully allocated.
Show sourceexamples/number-input/recipe-allocator.tsx
"use client";
import { useState } from "react";
import {
Field,
FieldControl,
FieldLabel,
NumberInput,
} from "@dethink/components";
const channels = [
{ id: "search", label: "Search ads" },
{ id: "social", label: "Social" },
{ id: "content", label: "Content" },
] as const;
type Allocation = Record<(typeof channels)[number]["id"], number>;
/**
* Cross-field validation: each input is fine on its own — the invariant
* lives across all three, so every field flips invalid together and a live
* region explains the group-level error.
*/
export function NumberInputRecipeAllocator() {
const [allocation, setAllocation] = useState<Allocation>({
search: 50,
social: 30,
content: 20,
});
const total = channels.reduce(
(sum, channel) => sum + (allocation[channel.id] || 0),
0,
);
const balanced = total === 100;
return (
<div className="mx-auto max-w-sm space-y-4">
<div className="grid grid-cols-3 gap-3">
{channels.map((channel) => (
<Field
key={channel.id}
id={`alloc-${channel.id}`}
invalid={!balanced}
>
<FieldLabel className="text-sm">{channel.label}</FieldLabel>
<FieldControl asChild>
<NumberInput
type="number"
min={0}
max={100}
value={allocation[channel.id]}
onChange={(event) =>
setAllocation((current) => ({
...current,
[channel.id]: Number(event.target.value) || 0,
}))
}
/>
</FieldControl>
</Field>
))}
</div>
<div className="space-y-1.5">
<div
aria-hidden="true"
className="bg-muted flex h-2 overflow-hidden rounded-full"
>
{channels.map((channel, index) => (
<div
key={channel.id}
className={
index === 0
? "bg-primary"
: index === 1
? "bg-info"
: "bg-success"
}
style={{ width: `${Math.min(allocation[channel.id], 100)}%` }}
/>
))}
</div>
<p
aria-live="polite"
className={`text-sm ${balanced ? "text-muted-foreground" : "text-destructive font-medium"}`}
>
{balanced
? "Budget fully allocated."
: `Allocations must total 100% — currently ${total}%.`}
</p>
</div>
</div>
);
}NumberInput renders a real input element, so all native numeric attributes apply.
| Prop | What it does | Default |
|---|---|---|
numberMode"decimal" | "numeric" | Sets inputMode so mobile keyboards show the right keypad — decimals or digits only. | "decimal" |
type"text" | "number" | Use "number" for native min/max/step semantics and arrow-key stepping; "text" keeps free-form entry with a numeric keypad. | "text" |
controlSize"sm" | "md" | "lg" | Height and typography scale aligned with Input. | "md" |
invalidboolean | Shows an error style and marks the field as invalid for screen readers. | false |
…native input propsInputHTMLAttributes | Renders a real input: min, max, step, name, disabled, and form behavior are native. | Not set |