Components · Forms & selection
Number input
Numeric input supporting integer and decimal (float) values with increment/decrement controls. Split from zetta-form-v1_4.md (removed in v1.5) in v1.4.
Preview
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Number input" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `number-input` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `number-input` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
┌──────────────────────────┬───┐
│ 42 │ ▲ │ ← increment stepper
│ │ ▼ │ ← decrement stepper
└──────────────────────────┴───┘
Stepper buttons are optional (showStepper={false} hides them). The field always accepts keyboard arrow keys regardless of stepper visibility.
| State | Visual |
|---|---|
| Default | {colors.border-strong} border, {colors.surface-card} fill |
| Hover | {colors.muted} border |
| Focus | {colors.primary} border + glow |
| Error | {colors.danger} border + glow |
| Disabled | Muted border, disabled fill, not-allowed cursor |
| Read-only | {colors.hairline} border, {colors.surface-muted} fill |
When to use
- Numeric entry where the value must be a number: quantities, ages, prices, dimensions.
- Fields where increment/decrement controls help the user adjust a value.
When not to use
- Free-form text with occasional numbers — use Text Input.
- Selecting from a range with a visual slider — use Range.
- Phone numbers, zip codes, credit cards — use Text Input with
type="tel"/inputmode="numeric".
Do
- Set
minandmaxconstraints when the valid range is known. - Use
step="0.01"or appropriate decimal precision for monetary or measured values. - Show the unit (kg, %, $) in a suffix or in the label — not inside the field.
Don't
- Don't use
type="number"for values that aren't truly numeric (phone, zip) — useinputmode="numeric"on a text input instead. - Don't omit
min/maxfor fields with logical bounds (e.g. quantity ≥ 1).
- Label: always visible, never placeholder-only.
- Placeholder: show expected format or example — "0", "1.5", "100".
- Unit: show unit in suffix or label — "Weight (kg)", not inline in the field.
- Error: be specific — "Value must be between 1 and 100." not "Invalid number."
- Stepper
aria-label: "Increase [field name]" / "Decrease [field name]".
Keyboard
| Key | Behavior |
|---|---|
Tab |
Focus input |
Up Arrow |
Increment by step |
Down Arrow |
Decrement by step |
Page Up |
Increment by step * 10 |
Page Down |
Decrement by step * 10 |
Home |
Set to min (if defined) |
End |
Set to max (if defined) |
Accessibility
- Use
type="number"— native semantics handle most AT behavior. - Add
aria-valuemin,aria-valuemax,aria-valuenowif using a custom implementation. - Always pair with a
<label>. Do not rely on placeholder as label. - Stepper buttons require
aria-label("Increase value", "Decrease value"). - Error state:
aria-invalid="true"andaria-describedby.
Spec
Values and token references resolved straight from the Zetta spec.
input-number
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- lineHeight
- 1.5
- height
- 36px
- paddingX
- {spacing.sm}
- paddingY
- 0
- width
- 100%
- textAlign
- left
- transition
- border-color {motion.duration-fast}, box-shadow {motion.duration-fast}
input-number-hover
- borderColor
- {colors.muted}
input-number-focus
- borderColor
- {colors.primary}
- outline
- none
- boxShadow
- 0 0 0 2px {colors.primary-bg}
input-number-disabled
- backgroundColor
- {colors.disabled-bg}
- textColor
- {colors.disabled-text}
- borderColor
- {colors.disabled-border}
- cursor
- not-allowed
input-number-error
- borderColor
- {colors.danger}
- boxShadow
- 0 0 0 2px {colors.danger-bg}
input-number-read-only
- backgroundColor
- {colors.surface-muted}
- textColor
- {colors.ink}
- borderColor
- {colors.hairline}
input-number-stepper
- display
- flex
- flexDirection
- column
- borderLeft
- 1px solid {colors.hairline}
- height
- 36px
input-number-stepper-button
- width
- 24px
- height
- 18px
- display
- flex
- alignItems
- center
- justifyContent
- center
- backgroundColor
- transparent
- textColor
- {colors.muted}
- cursor
- pointer
- borderRadius
- 0
input-number-stepper-button-hover
- backgroundColor
- {colors.surface-muted}
- textColor
- {colors.ink}
input-number-stepper-button-disabled
- textColor
- {colors.disabled-text}
- cursor
- not-allowed