Number field
Enter a bounded number with clear increment controls.
fieldnumeric
Preview
Number field preview
Live · keyboard ready
seats
When to use
- Bounded quantities like seats, items, or prices where the exact number matters.
- Values people nudge with steppers, arrow keys, or by dragging the label with scrub.
- Numbers with units, using a prefix or a pluralizing suffix function.
When not to use
- Use slider when the approximate position matters more than the exact number.
- Use input for numeric strings like phone numbers or codes.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "number-field",
"type": "registry:ui",
"title": "Number field",
"description": "Enter a bounded number with clear increment controls.",
"categories": [
"inputs"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/number-field/number-field.tsx",
"type": "registry:component",
"target": "@components/manicat/number-field/number-field.tsx"
},
{
"path": "registry/components/number-field/number-field.module.css",
"type": "registry:component",
"target": "@components/manicat/number-field/number-field.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/number-field",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/number-field",
"markdown": "https://uiarc.dev/components/number-field/markdown",
"tags": [
"field",
"numeric"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { useState } from "react";
import { NumberField } from "@/components/manicat/number-field/number-field";
export function NumberFieldExample() {
const [seats, setSeats] = useState(12);
return <NumberField label="Seats" value={seats} onValueChange={setSeats} min={1} max={64} scrub suffix=" seats" />;
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Seat picker Buy seats with a stepper, a scrubbable value and a running total.
Seat picker
Buy seats with a stepper, a scrubbable value and a running total. preview
Live · keyboard ready
seats
$144/month, billed to the workspace owner.
seat-picker-buy-seats-with-a-stepper-a-scrubbable-value-and-a-running-total-.tsxtsx
"use client";
import { useState } from "react";
import { NumberField } from "@/components/manicat/number-field/number-field";
export function NumberFieldSeatsExample() {
const [seats, setSeats] = useState(12);
return (
<div className="grid w-full max-w-xs gap-3">
<NumberField label="Seats" value={seats} onValueChange={setSeats} min={1} max={64} scrub suffix=" seats" />
<p className="text-sm text-(color:--text-muted)">${seats * 12}/month, billed to the workspace owner.</p>
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| ArrowUp / ArrowDown | Steps by step; holding repeats and speeds up, and keeps pushing at a limit. |
| Shift + Arrow / PageUp / PageDown | Steps by largeStep. |
| Home / End | Jumps to min or max when not typing. |
| Enter | Commits a typed draft, clamped to min and max, or selects the value. |
| Escape | Reverts a typed draft to the value before editing. |
Accessibility
- The input has role="spinbutton" with aria-valuenow, aria-valuemin, aria-valuemax, and aria-valuetext including prefix and suffix.
- Stepper buttons are labelled "Increase <label>" and "Decrease <label>" and marked aria-disabled at a limit.
- Changes are announced through a polite aria-live region, and a clamp says which limit it met ("10 seats, maximum"); rolling digits are aria-hidden.
- A typed value past a limit sets aria-invalid and adds the limit note to aria-describedby until it commits. The warning also uses copy, never color alone.
- Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focus darkens the shell border, and a focused stepper button takes its hover fill.
Motion
- Digits roll on wheels in the direction of change (up rolls up, down rolls down) with tabular numerals, and affixes reword in place.
- Holding a stepper repeats after 400ms and ramps from 150ms to 40ms per step.
- At a limit the value strains a few pixels toward the press and springs home; pushes in a row strain a little further, capped like overscroll. The refused button shakes once and the limit note rises in beside the label.
- A typed value past a limit tints the shell with the warning role; on Enter or blur it springs back to the limit with a digit roll.
- Reduced motion changes the value at once and answers a press at a limit with a brief warning tint on the number instead of the strain and shake.
Responsive behavior
- The control is min(100%, 196px) wide at md (164px at sm, 228px at lg), overridable with --number-field-width. The label row, with its limit note, follows the same width.
- Stepper buttons are 28, 34, or 40px by size with touch-action manipulation, so fast taps do not zoom.
- Scrubbing the label uses pointer capture with touch-action pan-y, so vertical scrolling still works on touch.
Performance
- Holding a stepper repeats on timeouts that speed up, not a per-frame loop.
- Two ResizeObservers handle digit layout and the helper row; fine for forms, not for large grids.
Notes for AI
- Use for bounded integers or decimals like quantities, seats, and prices. Use slider when the approximate position matters more than the exact number.
- Controlled with value and onValueChange or uncontrolled with defaultValue. There is no name prop; add a hidden input for plain forms.
- Set min below zero to allow negatives; the input then accepts a minus sign.
- The limit note sits at the end of the label row, so keep labels short enough to share the control's width with it, or set limitHint={false}.