Number Input
ProntoCampo numérico com botões +/− (stepper) e máscara monetária BRL. Validação inline, min/max, step.
Usar quando
Quantidade (ex: número de processos), valor monetário, dias, percentual, idade. Qualquer número discreto ou contínuo.
Não usar quando
Texto livre (use Text Input). Data (use Date Input). Slider numérico (use Slider). Telefone (use Text Input com máscara).
Variantes
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Stepper repeat | mousedown +/- | Incrementa imediatamente, depois acelera após 500ms | recursive |
| Mask format | onChange (monetário) | "1234567" → "12.345,67" | instant |
| Disabled stepper | value no min/max | Botão correspondente disabled (opacity 0.4) | instant |
| Scroll to change | mouse wheel sobre input em foco | Aumenta/diminui pelo step | instant |
| Validate range on blur | blur com value fora min/max | Clamp para o limite mais próximo | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Foco no input |
| ↑ / ↓ | Incrementa/decrementa pelo step |
| PgUp / PgDn | Step × 10 |
| Home / End | Min / Max |
| Digitar números | Substitui valor |
ARIA esperado
- role="spinbutton"
- aria-valuenow / valuemin / valuemax / valuetext
- aria-label="Quantidade de processos"
- inputMode="numeric" ou "decimal"
Notas
- inputMode="decimal" para monetário (mostra teclado com vírgula em mobile).
- Sempre exiba unidade (R$, %, kg) próximo ou prefix/suffix.
- Stepper +/- com auto-repeat (acelera ao segurar).
- Validate clamp no blur (não impede digitação).
Código
'use client';
import { useState } from 'react';
import { Plus, Minus } from 'lucide-react';
import { T, RADIUS } from '@/lib/tokens';
export function NumberInput({ value, onChange, min, max, step = 1 }) {
function clamp(v: number) {
if (min != null) v = Math.max(min, v);
if (max != null) v = Math.min(max, v);
return v;
}
return (
<div style={{ display: 'inline-flex', border: `1px solid ${T.border}`, borderRadius: RADIUS.md }}>
<button onClick={() => onChange(clamp(value - step))}><Minus size={12} /></button>
<input type="number" value={value} inputMode="numeric"
onChange={(e) => onChange(clamp(Number(e.target.value)))}
onWheel={(e) => onChange(clamp(value + (e.deltaY < 0 ? step : -step)))} />
<button onClick={() => onChange(clamp(value + step))}><Plus size={12} /></button>
</div>
);
}Regras
Faça
- ✓Stepper +/- sempre visível.
- ✓Auto-repeat ao segurar +/- (acelera após 500ms).
- ✓inputMode="numeric" ou "decimal" pra mobile.
- ✓Clamp no blur (não impede digitação).
- ✓Mostre unidade (R$, %, kg).
- ✓Mono + tabular-nums no display.
Não faça
- ✗Não use step=0 (sem sentido).
- ✗Não permita texto não-numérico.
- ✗Não force vírgula vs ponto sem flex (i18n).
- ✗Não ofereça stepper sem keyboard ↑↓.
- ✗Não use scroll-to-change sem foco (interfere com page scroll).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background |
T.border | #E3DFD2 | borda |
T.borderInk | #000000 | focus |
T.surfaceHover | #F7F5EC | hover dos +/- |
RADIUS.md | 12 | borderRadius |
FONT.mono | JetBrains Mono | value display |