Number Input & Stepper
Number Input & Stepper
8 reglasUsa type="text" inputmode="numeric" en lugar de type="number"
El control nativo <input type="number"> arrastra problemas de usabilidad y accesibilidad que rara vez compensa: el scroll del ratón sobre el campo incrementa el valor por accidente, Chrome descarta letras en silencio sin avisar al usuario, NVDA lo anuncia como campo sin etiqueta y Dragon NaturallySpeaking no lo reconoce. Por eso GOV.UK migró toda su plataforma de millones de usuarios a type="text" con inputmode="numeric": ese par activa el teclado numérico en móvil sin el comportamiento del spinbutton nativo. Reserva type="number" solo para campos genuinamente incrementables con rango acotado donde tu research lo justifique.
<input type="text" inputmode="numeric" pattern="[0-9]*" autocomplete="off">
<input type="number"> // spinner nativo + flechas
Cinco categorías de datos nunca deben usar type="number"
La spec HTML reserva type="number" para "números incrementables". Los datos que parecen numéricos pero no se incrementan deben ir en type="text" con el inputmode adecuado. Con type="number", los navegadores intentan redondear cifras grandes y las convierten a notación exponencial, y truncan los ceros a la izquierda. Las cinco categorías que nunca deben usarlo son: tarjetas de crédito (16 dígitos), números de documento o pasaporte (con ceros iniciales), códigos postales (muchos empiezan por 0), teléfonos (espacios, guiones, +) y cualquier número muy grande con riesgo de redondeo silencioso.
El stepper (− valor +) solo para cantidades pequeñas y acotadas
El quantity stepper con botones − y + es el control correcto cuando el rango es pequeño (típicamente 1–10), el contexto es táctil (carrito, asientos, número de personas) y el usuario rara vez salta más de 2–3 unidades. Para cantidades grandes, precios, años o cualquier valor arbitrario, un stepper sin campo editable obliga a docenas de taps. Baymard documentó errores reales en checkout cuando el stepper no permite escribir directo: en Williams Sonoma el "1" por defecto se concatenó con la nueva entrada y guardó "21" en lugar de "2"; en West Elm el usuario debía borrar el valor antes de escribir. Un stepper puro es adecuado solo si el máximo esperado es bajo; en otro caso, añade un campo central editable.
Baymard Institute (Cart & Checkout Usability)Escribe 24 de un toque, o ajusta con − / +.
Para pedir 24 unidades → 23 taps.
Touch target mínimo de 44×44 px en los botones − y +
Los botones de incremento y decremento son targets pequeños que exigen precisión; por debajo de cierto tamaño disparan errores y rage taps. WCAG 2.5.5 (AAA) pide al menos 44×44 px y WCAG 2.5.8 (AA) fija el mínimo absoluto en 24×24 px. Las guías de plataforma convergen en el límite operativo más alto: Apple HIG recomienda 44×44 pt y Material 3, 48dp de altura. El área táctil puede extenderse con padding sin alterar el tamaño visual del botón, y conviene dejar al menos 8 px de separación entre − y + para que el dedo no active el botón contrario.
WCAG 2.5.5 / 2.5.8 · Apple HIG (44pt) · Material Design 3 (48dp) · Nielsen Norman (touch-target size)Edición directa del campo; selecciona todo el contenido al hacer focus
Un stepper que solo acepta los botones − y + frustra al usuario cuando necesita saltar valores. El campo central debe ser editable y debe seleccionar todo su contenido en el evento focus (vía input.select()), eliminando la necesidad de borrar el valor existente antes de escribir. Baymard observó que no hacerlo es una de las causas más frecuentes de errores de cantidad en checkout: en West Elm el usuario tenía que borrar primero, y en Williams Sonoma el default "1" se concatenó con la nueva cifra produciendo "21" en vez de "2". Pre-seleccionar el valor permite sobrescribir de inmediato.
Escribe "24" y reemplaza al instante.
onfocus="this.select()"Hay que borrar antes de escribir.
Resultado típico: "21" en vez de "2".
Desactiva el botón en el límite y comunícalo con texto, no solo color
Cuando el usuario alcanza el mínimo o el máximo, el botón correspondiente debe desactivarse con disabled real (o aria-disabled="true"), no solo con una opacidad menor. Deshabilitar únicamente con CSS deja al usuario de tecnología asistiva sin saber por qué el botón no responde. El estado del límite debe comunicarse también por texto: una nota visible o un aria-describedby en el campo que indique el rango ("Mínimo: 1 unidad", "Rango: 1 a 99"). Así el lector de pantalla anuncia "disminuir, botón, desactivado" y el usuario entiende el porqué.
Custom stepper: role="spinbutton" con foco solo en el campo
Un stepper construido con divs o botones no semánticos necesita el rol ARIA correcto para que la tecnología asistiva lo entienda como control de rango. MDN recomienda usar el <input type="number"> nativo con min/max siempre que sea posible; el rol custom es solo para cuando el HTML semántico no alcanza. En ese caso, el campo central lleva role="spinbutton" con aria-valuenow, aria-valuemin, aria-valuemax y un nombre accesible. El foco de teclado reside únicamente en el campo: los botones − y + llevan tabindex="-1" y se accionan con clic o con las flechas (↑/↓ ±1, Home/End van a min/max).
<!-- Botones fuera del tab order --> <button tabindex="-1" aria-label="Disminuir">−</button> <input role="spinbutton" aria-valuenow="3" aria-valuemin="1" aria-valuemax="99" aria-labelledby="qty-label" type="text" inputmode="numeric"> <button tabindex="-1" aria-label="Aumentar">+</button>
<!-- Sin roles, todo en el tab order --> <div class="stepper"> <div tabindex="0">−</div> <span>3</span> <div tabindex="0">+</div> </div> // El AT no sabe que es un control numérico. // El valor "3" nunca se anuncia.
Para decimales usa inputmode="decimal"; nunca asumas el separador
El separador decimal varía por locale: punto en EE.UU. y Reino Unido, coma en España, México y Alemania. <input type="number"> no controla este comportamiento de forma consistente entre navegadores y puede rechazar entradas válidas. Con type="text" inputmode="decimal" el teclado muestra la tecla decimal nativa de la plataforma y acepta cualquier formato, dejando el parseo a tu código: normaliza tanto "," como "." antes de parseFloat(), sin asumir el locale del navegador. Para mostrar valores formateados (no editar) usa Intl.NumberFormat en vez de manipular strings. Para entrada de negativos opcionales, GOV.UK advierte que algunos teclados táctiles bloquean el signo, así que usa type="text" sin inputmode.
inputmode="decimal" muestra la tecla decimal nativa.Normaliza "," y "." antes de
parseFloat().navegador lo descarta por separador "incorrecto".
- R-427 Usa type="text" inputmode="numeric" en lugar de type="number"
- R-428 Cinco categorías de datos nunca deben usar type="number"
- R-429 El stepper (− valor +) solo para cantidades pequeñas y acotadas
- R-430 Touch target mínimo de 44×44 px en los botones − y +
- R-431 Edición directa del campo; selecciona todo el contenido al hacer focus
- R-432 Desactiva el botón en el límite y comunícalo con texto, no solo color
- R-433 Custom stepper: role="spinbutton" con foco solo en el campo
- R-434 Para decimales usa inputmode="decimal"; nunca asumas el separador