Un solo campo para los códigos de verificación
La mayoría de campos de código de verificación son seis inputs, uno por dígito. Funcionan si escribes el código a mano, carácter a carácter. En cuanto pegas el código o dejas que el móvil lo rellene, fallan.
Al diseñar @teselia/otp, el segundo componente de Teselia, descarté ese modelo desde el principio.
Los problemas de seis campos
- El lector de pantalla anuncia seis campos sin nombre en lugar de uno con su etiqueta.
- Al pegar, el código entero cae en la primera casilla, salvo que alguien escriba código frágil para repartirlo.
- El móvil, cuando te sugiere el código que acaba de llegar, rellena un único campo. Con seis, a menudo no rellena ninguno.
- El script tiene que mover el foco en cada tecla, y cada implementación resuelve a su manera el borrado entre casillas.
Lo del autocompletado afecta al cumplimiento. El criterio 3.3.8 de WCAG 2.2, Autenticación accesible, pide que nadie tenga que transcribir un código carácter a carácter para identificarse. Si no puedes pegarlo ni autocompletarlo, el campo no cumple.
Un campo real debajo de las casillas
El componente tiene un único <input> con el código completo. Las casillas que ves son un dibujo: elementos que los lectores de pantalla ignoran y que copian el valor del campo, un carácter en cada una. Encima coloqué el campo real con el texto y el cursor transparentes, y ese campo recibe el ratón y el teclado.
Para que se comporte como casillas, cuando el cursor cae en una casilla llena, el componente selecciona ese carácter. Si escribes, lo sustituyes, y el código nunca pasa de su longitud.
La prueba que hice antes de escribir código
Los Web Components guardan su contenido en un Shadow DOM, y yo no sabía si el iPhone ofrecería el código recibido para un campo que vive ahí dentro. Si no lo hacía, el componente no tenía sentido.
Preparé una página de prueba y me envié un correo con un código. El teclado del iPhone lo sugirió en los tres campos de la página, incluido el del componente. Con esa respuesta, empecé a escribir el resto.
Detalles que aparecieron al probar
maxlengthestropeaba el pegado. El navegador recorta el texto antes de que el componente lo limpie, así que al pegar123 456el espacio ocupaba un hueco y perdías el último dígito. Quitémaxlength, y ahora el componente recorta después de quitar espacios y guiones. Un test pega a través del portapapeles real y falla si alguien lo vuelve a poner.- Dígitos de otras escrituras. Los teclados japonés y chino pueden escribir dígitos de ancho completo, y la escritura árabe tiene sus propios dígitos. El componente convierte todos a
0-9. - Una flecha mantenida. En Brave, si mantenías pulsada una flecha, el cursor avanzaba pero la casilla activa se quedaba quieta hasta que la soltabas. Lo encontré en las pruebas a mano y ya lo he corregido.
- Teclados con predicción. Si el componente reescribe el campo mientras el teclado del móvil compone una palabra, rompe el teclado. Ahora espera a que termine.
Envío automático, desactivado por defecto
Muchos campos de este tipo envían el formulario al escribir el último dígito. Resulta cómodo, pero te cambia de página sin que lo pidas, y WCAG 2.2 exige avisar antes de un cambio así.
Teselia lo trae desactivado. Si lo activas con el atributo autosubmit, aparece bajo las casillas un aviso visible de que el código se enviará al escribir el último dígito. El mismo aviso forma parte de la descripción del campo, y los lectores de pantalla también lo leen.
Lo que no salió como quería
El gestor de contraseñas que probé rellenaba códigos en campos de tipo contraseña, y en un campo de texto no. Podría haber cambiado el tipo del campo, pero entonces verías puntos en lugar del código, los lectores de pantalla leerían caracteres ocultos y el navegador te ofrecería guardarlo como contraseña. Lo dejé como campo de texto. Puedes copiar el código desde el gestor y pegarlo en un paso.
El documento de diseño explica el razonamiento completo, y la documentación incluye una demo.
Teselia: componentes de formulario accesibles, de uno en uno
Desde finales de septiembre trabajo en Teselia, un proyecto paralelo de código abierto. Teselia reúne componentes de formulario accesibles: piezas pequeñas, cada una con un solo trabajo, que puedes usar en cualquier web.
El proyecto
Cada componente resuelve un campo de formulario que suele dar problemas, como el teléfono con prefijo internacional o el código de verificación. Escribo los componentes con Vue 3 y los publico como Web Components. Puedes usarlos en una página HTML sin framework, en React, en Vue o dentro de WordPress.
Los componentes trabajan con el <form> nativo igual que un <input>: el navegador los valida, los restablece y los autocompleta sin código extra.
El nombre viene de tessera, la pieza con la que los romanos hacían mosaicos. En Roma, una tessera también servía de ficha para entrar en un lugar. Me gustaron las dos ideas: piezas pequeñas que forman un conjunto, y acceso para cualquiera.
Lo que ya está publicado
- @teselia/phone es un campo de teléfono internacional. Eliges el país escribiendo su nombre, su código o su prefijo, y tu servidor recibe el número en formato internacional (
+34612345678). Publiqué la versión 1.0 el 27 de septiembre. - @teselia/otp es un campo para códigos de verificación. Ves seis casillas, pero por dentro hay un único campo de texto: puedes pegar el código o dejar que el móvil lo rellene, y el lector de pantalla lo lee como un solo campo. Publiqué la versión 1.0 el 7 de octubre.
Ahora trabajo en el tercero, un campo de contraseña.
Mi forma de trabajar
Antes de escribir código, redacto un documento de diseño para cada componente. Ahí apunto cada decisión, las alternativas que descarté y el motivo. Si cambio de idea durante el desarrollo, actualizo el documento en el mismo pull request. Los tienes todos en la carpeta design/ del repositorio.
Quiero que cumplan WCAG 2.2 AA sin configurar nada. Cada componente pasa tests automáticos en Chromium, Firefox y WebKit, y axe-core revisa cada estado: abierto, cerrado, con error o deshabilitado. Después lo pruebo a mano con teclado, con el lector de pantalla NVDA, en modo de alto contraste y con el navegador ampliado al 400 %. Todavía no lo he probado con VoiceOver, y la documentación lo indica.
Claude Code escribe gran parte del código
Uso Claude Code, el asistente de programación de Anthropic, para generar la mayor parte del código de Teselia. Puedes comprobarlo en GitHub: casi todos los commits lo llevan como coautor.
Yo decido qué construimos y cómo, reviso los documentos de diseño y fijo las reglas del proyecto, como no copiar código de otras librerías o pasar los tests en los tres navegadores antes de fusionar un cambio. También hago las pruebas que una IA no puede hacer: recorrer un componente con un lector de pantalla o comprobar en mi iPhone que el teclado ofrece el código que acaba de llegar.
Prefiero que lo leas aquí antes que en el historial de commits.
Enlaces
- Documentación y demos: teselia.caicesardev.com
- Código: github.com/caicesardev/teselia
- Paquetes en npm: @teselia/phone y @teselia/otp
Teselia es código abierto con licencia MIT. Si encuentras un fallo, sobre todo de accesibilidad, abre un issue en GitHub o escríbeme.
Construir un campo de teléfono accesible
Un campo de teléfono parece lo más sencillo de un formulario hasta que tiene que aceptar números de otros países. Entonces necesitas un selector de prefijo, y los que encuentras por ahí suelen ser listas de banderas que no puedes recorrer con teclado ni con un lector de pantalla.
Repaso las decisiones que tomé al construir @teselia/phone, el primer componente de Teselia, y el motivo de cada una.
Un solo valor para el servidor
El formulario envía el número en formato E.164: +34612345678, sin espacios y con el prefijo incluido. No envío el país aparte porque ya va dentro del número.
Tú puedes escribirlo a tu manera: 612 34 56 78, 612-34-56-78 o con 0034 delante. El componente lo interpreta. Si dejara esa tarea al servidor, cada backend acabaría con sus propias reglas.
Elegir el país escribiendo
El selector de país es un combobox editable: escribes y la lista se filtra. Sigue el patrón de la guía de prácticas de autoría de WAI-ARIA, el que esperan los lectores de pantalla.
Puedes buscar por nombre sin preocuparte de las tildes (espana encuentra España), por código ISO (gb) o por prefijo: con 3 quedan los países del +30 al +39, y con 34, España.
Con las flechas recorres la lista, Enter elige y Escape cierra sin cambiar nada. El navegador aporta los nombres de los países con Intl.DisplayNames, en el idioma de la página, así que el componente no carga ningún archivo de traducciones.
Puedes marcar países sugeridos para que aparezcan arriba, en un grupo propio. No los repito en la lista completa: si España aparece dos veces, el lector de pantalla cuenta una opción de más y la posición que anuncia deja de cuadrar.
Sin banderas
Quizá echas de menos las banderas. Una bandera no aporta nada a quien no ve la pantalla, porque el nombre del país y el prefijo ya lo identifican. Además, Windows no muestra las banderas en emoji: en su lugar ves dos letras, «ES». Y dibujar unas 240 banderas propias habría retrasado la primera versión.
El selector muestra España +34. El prefijo no basta por sí solo: el +1 lo comparten Estados Unidos, Canadá y otros países, y el +44 el Reino Unido, Jersey, Guernsey y la isla de Man. El código ISO tampoco funciona, porque los lectores de pantalla lo pronuncian como una palabra y leen «IT» como el pronombre inglés «it».
Pegar un número completo
Si pegas +44 20 7946 0958, el componente detecta el prefijo, cambia el país a Reino Unido, deja en el campo la parte nacional y avisa del cambio al lector de pantalla. El autocompletado del navegador sigue el mismo camino.
Ese aviso me dio un problema que encontré probando a mano. Si pegaba dos veces seguidas un número del mismo país, el segundo aviso no llegaba a NVDA en Brave. Chromium agrupa los cambios de accesibilidad por fotograma, y si el componente vaciaba y rellenaba el mensaje en el mismo instante, Chromium no veía ningún cambio. Lo arreglé esperando 100 milisegundos entre vaciarlo y escribirlo.
Errores en el momento justo
Si un campo se pone rojo cuando acabas de empezar a escribir, te riñe por algo que todavía no has terminado. Por eso el componente no muestra errores mientras escribes. Los muestra al salir del campo, si has escrito algo, o al enviar el formulario. A partir de ahí, el mensaje se actualiza contigo y desaparece cuando el número es válido. El borde también pasa de 1 a 2 píxeles, para que no dependas del color para ver el error.
Limitaciones conocidas
- Un
<label for>externo no llega al campo interno, porque el Shadow DOM no deja cruzar esas referencias. Usa el atributolabeldel componente. - Para que pese poco, valido con los metadatos reducidos de
libphonenumber-js. Detectan prefijos imposibles y longitudes absurdas, pero aceptan, por ejemplo, un móvil alemán al que le falta un dígito.
Tampoco lo he probado todavía con VoiceOver.
El documento de diseño recoge todas estas decisiones con las alternativas que descarté, y la documentación tiene una demo para probarlo.

