Number Input

Pronto

Campo 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

R$
%
meses

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Stepper repeatmousedown +/-Incrementa imediatamente, depois acelera após 500msrecursive
Mask formatonChange (monetário)"1234567" → "12.345,67"instant
Disabled steppervalue no min/maxBotão correspondente disabled (opacity 0.4)instant
Scroll to changemouse wheel sobre input em focoAumenta/diminui pelo stepinstant
Validate range on blurblur com value fora min/maxClamp para o limite mais próximoinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco no input
↑ / ↓Incrementa/decrementa pelo step
PgUp / PgDnStep × 10
Home / EndMin / Max
Digitar númerosSubstitui 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

TokenValorPapel
T.surface#FFFFFFbackground
T.border#E3DFD2borda
T.borderInk#000000focus
T.surfaceHover#F7F5EChover dos +/-
RADIUS.md12borderRadius
FONT.monoJetBrains Monovalue display