Introducción
Still & Spoke organiza pequeños talleres de cerámica los sábados. La página del estudio está preparada, pero el formulario necesita etiquetas visibles, comprobaciones nativas, una nota de asistencia condicional y un diseño utilizable con teclado en una pantalla estrecha. Harás una reserva local real, además de mostrar un mensaje. Antes de cambiar tu trabajo, Study form ofrece un ejemplo completo e independiente.
Necesitas elementos básicos de HTML, selectores y diseño CSS, y objetos, funciones, selección del DOM y eventos de JavaScript. Este laboratorio presenta asociaciones de etiquetas, restricciones del navegador, descripciones accesibles, required condicional, foco visible y las reglas adaptables utilizadas aquí. En este primer laboratorio, un controlador de envío y una API local preparados procesan las solicitudes; el siguiente construye ese controlador. Todas las personas y direcciones son sintéticas; no se envía correo ni se cobra nada. Es una práctica de usabilidad, no una certificación completa de accesibilidad. Los 50 minutos son una estimación del autor que incluye lectura y errores habituales.
Usa Workspace para los comandos y Studio para el sitio. Puedes editar /home/labex/project/stillspoke/form.html, validity.js y form.css. Conserva los demás archivos, la API preparada, las reglas de capacidad y los visitantes existentes. Cada laboratorio se inicia por separado. El servidor coloca el contenido exacto de form.html en la estructura visual de la página; no añade etiquetas ausentes ni regenera tu trabajo. Después de actualizar, espera a que termine la carga antes de usar el formulario. Las etiquetas del sitio siguen en inglés en todos los idiomas de la documentación.
Da un nombre visible a cada control
En este paso, utiliza el ejemplo completo con el teclado y después conecta cada control de tu página con su etiqueta visible y sus instrucciones.
En Studio, elige el enlace Study form. Navegas dentro del sitio; no cambias de Tab de LabEx. El ejemplo tiene su propia API y base de datos: no completa tu trabajo ni ocupa sus plazas. Pulsa Tab hasta que Your name tenga un contorno visible. Escribe Example Guest, pulsa Tab y escribe sample@example.test. Pulsa Tab hasta Session, pulsa C para elegir Clay, slowly / 10:00 y lee el título seleccionado antes de continuar. Este selector nativo permite escribir el comienzo de una opción. Pulsa Tab hasta la casilla de asistencia, Tab hasta la nota opcional y Tab hasta Reserve my place. Pulsa Enter una vez. Espera a que Submission result muestre Reserved for Example Guest y Confirmation SS-… con un número emitido por el servidor. El mensaje pasivo de envío desaparece. La sección de resultado recibe el foco para facilitar su localización.
La imagen muestra el resultado con su propio encabezado y una confirmación real. Tu número será distinto.

Elige Studio records en el ejemplo. Busca el mismo nombre, dirección, horario legible y número de confirmación. Esta página lee registros aceptados, no el texto del resultado del navegador. Elige Back to signup y después Work form para volver a tu página. Sigue incompleta y no tiene ninguna reserva de trabajo nueva; el ejemplo no cuenta.
Cambia a Workspace. cd cambia el directorio actual de esa terminal; una ruta absoluta comienza por /. pwd muestra ese directorio. Estos comandos dejan explícito el destino de la edición:
cd /home/labex/project/stillspoke
pwd
Salida esperada:
/home/labex/project/stillspoke
Los valores name del formulario son las claves de la API: name, email, session, support y note. Un id de HTML identifica un elemento de la página. El valor for de una etiqueta apunta al id de su entrada, de modo que pulsar la etiqueta visible o leerla mediante tecnología de asistencia identifica el control. Aquí Your name utiliza for="person", mientras que la clave enviada es name: son atributos con funciones distintas. Encerrar la casilla en su etiqueta crea una asociación implícita. autocomplete="name" y autocomplete="email" identifican qué información puede sugerir el navegador; por sí solos no envían datos. El texto de marcador de posición desaparece al escribir y no sustituye una etiqueta visible.
required rechaza un valor vacío antes de enviar una solicitud. type="email" pide al navegador rechazar una sintaxis incorrecta; no demuestra a quién pertenece la dirección. minlength="2" exige al menos dos caracteres en el nombre. La primera opción vacía obliga a elegir una sesión real. El value de una opción es su identidad estable en el servidor; el título visible es para las personas. No deduzcas esa relación de su posición.
aria-describedby contiene ID separados por espacios. Asocia cada campo con su ayuda persistente y un párrafo de error inicialmente vacío. La ayuda explica la regla antes del fallo; el error añade el motivo tras un rechazo del servidor. Usa texto claro además del color. aria-hidden="true" oculta únicamente la flecha decorativa a las tecnologías de asistencia. ↗ es una referencia numérica HTML: su fuente ASCII se representa como una flecha. El párrafo Sending tiene role="status" para avisos pasivos. aria-labelledby de la sección de resultado apunta al ID de su encabezado y le da nombre. El atributo inicial hidden mantiene el resultado vacío fuera de la vista hasta que haya algo que mostrar. La sección independiente tiene tabindex="-1": un script puede enfocarla sin añadir una parada al orden habitual de Tab. No enfocamos un aviso de estado.
El siguiente comando reemplaza solo form.html. cat escribe el texto que sigue; > reemplaza el archivo indicado. El marcador HTML entre comillas después de << inicia un here-document: todo lo que aparece hasta la línea que contiene únicamente HTML es contenido literal. Las comillas impiden que el shell interprete caracteres del HTML. Pega el bloque completo, incluido el marcador final. Cuando vuelve el indicador del shell, la escritura ha terminado; no hay otra tecla para guardar. No pegues las comillas invertidas de Markdown. Los pequeños archivos JavaScript y CSS posteriores utilizan el mismo procedimiento.
cd /home/labex/project/stillspoke
cat > form.html <<'HTML'
<form id="signup">
<div class="pair">
<div class="field">
<label for="person">Your name (required)</label>
<input id="person" name="name" required minlength="2" autocomplete="name"
aria-describedby="name-help name-error">
<small id="name-help">At least two characters.</small>
<p class="error" id="name-error"></p>
</div>
<div class="field">
<label for="email">Email (required)</label>
<input id="email" name="email" type="email" required autocomplete="email"
aria-describedby="email-help email-error">
<small id="email-help">Practice address: you@example.test</small>
<p class="error" id="email-error"></p>
</div>
</div>
<div class="field">
<label for="session">Session (required)</label>
<select id="session" name="session" required aria-describedby="session-help session-error">
<option value="">Choose a time</option>
<option value="clay-morning">Clay, slowly / 10:00</option>
<option value="clay-afternoon">Clay, slowly / 14:00</option>
</select>
<small id="session-help">Choose a listed time. The afternoon session is full.</small>
<p class="error" id="session-error"></p>
</div>
<label class="check"><input type="checkbox" name="support"> I need seating assistance</label>
<div class="field">
<label for="note">Seating note (required when assistance is selected)</label>
<textarea id="note" name="note" rows="2" aria-describedby="note-help note-error"></textarea>
<small id="note-help">When assistance is selected, describe the help in at least 5 characters.</small>
<p class="error" id="note-error"></p>
</div>
<button type="submit">Reserve my place <span aria-hidden="true">↗</span></button>
<p id="progress" role="status"></p>
<section id="result" aria-labelledby="result-heading" tabindex="-1" hidden>
<h3 id="result-heading">Submission result</h3>
<p id="message"></p>
</section>
</form>
HTML
Vuelve a Studio y actualiza la página de trabajo. Lee las etiquetas incluso después de escribir. Pulsa Email para ver cómo el foco pasa a su entrada y luego Tab para seguir el orden del documento. Con el formulario vacío, activa Reserve my place: el navegador debe enfocar Your name y explicar que es obligatorio. Escribe un carácter y vuelve a intentarlo para observar la longitud mínima. Usa un nombre como Morgan Vale; escribe not-an-email para observar el rechazo de formato del correo. Son mensajes de validación nativa, no respuestas HTTP 422: el navegador todavía no ha enviado una reserva. Para corregir un valor, selecciona el texto con Ctrl+A o Cmd+A mientras el campo tiene el foco y escribe el reemplazo. Un error ortográfico no requiere reiniciar la VM.
Por ahora la nota sigue siendo opcional incluso con la asistencia marcada. Esa es la siguiente conducta que añadirás. La página tampoco tiene aún el diseño para pantallas estrechas. Autocomprobación: los cinco controles tienen etiquetas visibles y asociadas, la ayuda permanece visible y no se pueden enviar valores obligatorios vacíos.
Haz que la nota dependa de la casilla
En este paso, haz obligatoria la nota solo cuando se solicita asistencia de asiento, conservando todo lo escrito.
Una casilla expone el booleano checked: true si está seleccionada y false si no. Una entrada o área de texto expone la propiedad required correspondiente a su restricción HTML. STUDIO es un objeto de configuración preparado, cargado por scenario.js; su noteMin es 5 para este taller. Lee ese número en vez de suponer que todos los talleres futuros serán iguales. La configuración aporta datos del escenario, no implementa tu regla condicional.
querySelector devuelve el primer elemento que coincide con un selector CSS; [name="support"] y [name="note"] coinciden con esos atributos. Seleccionamos los controles por su name de API. La función actualiza dos restricciones nativas cuando cambia la casilla. En condition ? first : second, el operador condicional elige first si la condición es verdadera y second en caso contrario. La nota tiene así una longitud mínima de cinco al solicitar asistencia y de cero si no. Llamar una vez a updateNote() también inicializa el estado al cargar la página. El listener de change la ejecuta de nuevo al cambiar la casilla. Modificamos restricciones, no .value, por lo que desmarcar no borra el borrador.
En Workspace, reemplaza validity.js. El marcador JS funciona como HTML en el paso anterior:
cd /home/labex/project/stillspoke
cat > validity.js <<'JS'
const support = document.querySelector('[name="support"]');
const note = document.querySelector('[name="note"]');
function updateNote() {
note.required = support.checked;
note.minLength = support.checked ? STUDIO.noteMin : 0;
}
support.addEventListener('change', updateNote);
updateNote();
JS
Vuelve a Studio y actualiza la página. Con Tab, introduce un nombre de al menos dos caracteres, una dirección de sintaxis válida como morgan@example.test y Clay, slowly / 10:00. Pulsa Space sobre I need seating assistance para marcarla. Deja la nota vacía y activa Reserve my place. El navegador debe enfocar ahora Seating note y pedir un valor. Escribe near, sal con Tab e intenta enviar: cuatro caracteres son menos de los cinco requeridos. minlength se aplica al texto introducido por el usuario; aquí se escribe normalmente, sin asignarlo desde un script.
Vuelve a la casilla con Shift+Tab según sea necesario y pulsa Space para desmarcarla. La nota es opcional y near permanece. Marca de nuevo la asistencia y reemplaza la nota por A seat near the entrance. Conserva estas entradas para el siguiente paso; actualizar descarta lo no enviado, pero cambiar de Tab de LabEx mantiene abierta esta página.
Si la casilla parece no hacer nada, vuelve a Workspace y muestra el archivo guardado exacto:
cd /home/labex/project/stillspoke
cat validity.js
Compáralo con el bloque corto anterior, incluida la escritura de checked y la llamada final updateNote();. Si hace falta, reemplázalo con el bloque completo y actualiza Studio. Que falte un script o haya un error JavaScript es distinto de que el navegador rechace una nota vacía; no afirmes que la restricción funciona solo porque no se reservó nada. Autocomprobación: la casilla cambia la obligatoriedad, las notas cortas se rechazan solo cuando está marcada y el texto existente se conserva.
Mantén legible el formulario y visible su foco
En este paso, termina el diseño, realiza una reserva real en tu página y lee su registro aceptado.
Un formulario de dos columnas necesita poder plegarse. 1fr 1fr de CSS Grid da partes iguales al nombre y al correo; min-width: 0 permite reducir los campos. Con un ancho máximo de ventana de 800 píxeles, la media query cambia ese par a una columna. width: 100% junto con box-sizing: border-box incluye relleno y bordes en el ancho disponible. El selector input:not([type="checkbox"]) excluye las casillas de esa regla de ancho completo, conservando su pequeño cuadrado. El texto se puede leer sin pasar el puntero. La estructura preparada ya coloca la historia encima del formulario en pantallas pequeñas; estas reglas hacen que el formulario quepa dentro.
:focus-visible proporciona un contorno visible al foco del teclado. El color no es la única señal: también importan el texto obligatorio, los errores escritos y la forma del contorno. Un borde o una sombra de foco claramente equivalentes son válidos; eliminar toda indicación de foco no lo es. [aria-invalid="true"] da estilo a un campo marcado por el servidor; .error:empty oculta solo un párrafo de error vacío, no una etiqueta o explicación necesaria. overflow-wrap: anywhere permite partir una confirmación o dirección sintética larga. La preferencia de movimiento reducido evita añadir movimientos suaves a quien pide menos animación; este formulario no necesita animarse.
En Workspace, reemplaza form.css:
cd /home/labex/project/stillspoke
cat > form.css <<'CSS'
form { margin-top: 28px; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { min-width: 0; margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; font-size: 13px; font-weight: bold; }
input:not([type="checkbox"]), select, textarea {
width: 100%; box-sizing: border-box; padding: 11px 12px;
border: 1px solid #89988a; border-radius: 3px;
font: inherit; font-size: 15px; color: #243d35; background: #fcfaf5;
}
small { display: block; margin-top: 6px; font-size: 11px; color: #56645a; }
.check { display: flex; gap: 9px; align-items: center; margin: 20px 0; }
.check input { width: 18px; height: 18px; accent-color: #244c3d; }
button {
padding: 14px 20px; border: 0; border-radius: 3px;
font: inherit; font-size: 14px; color: #fffdf6; background: #254f41; cursor: pointer;
}
button[type="submit"] { width: 100%; display: flex; justify-content: space-between; }
button:disabled { background: #5b6c5e; cursor: wait; }
:focus-visible { outline: 3px solid #ad4d28; outline-offset: 4px; }
[aria-invalid="true"] { border: 2px solid #9d3327; }
.error { color: #963024; font-size: 12px; margin: 5px 0; }
.error:empty { display: none; }
#progress, #message { font-size: 14px; overflow-wrap: anywhere; }
#result { margin-top: 20px; border-top: 1px solid #c9cebf; padding-top: 12px; }
#result h3 { font-size: 15px; margin: 0 0 8px; }
#records article { border-top: 1px solid #c9cebf; overflow-wrap: anywhere; padding: 12px 0; }
@media (max-width: 800px) { .pair { grid-template-columns: 1fr; gap: 0; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
CSS
Vuelve a Studio, actualiza y utiliza tu formulario. Introduce Morgan Vale, morgan@example.test, elige Clay, slowly / 10:00 con C, marca la asistencia con Space y escribe A seat near the entrance. Pulsa Tab hasta el botón y Enter una vez. Espera la confirmación real del controlador preparado. Este primer laboratorio proporciona ese controlador: tú has implementado la semántica, las restricciones y el diseño que lo rodean. El siguiente explica y construye su lógica de solicitud y respuesta.
Elige Studio records y busca el mismo nombre, correo, horario y número. Conserva Existing Visitor y Avery Reed. Sus registros son independientes de tu nueva reserva; borrarlos para liberar plazas queda fuera de la tarea. El servidor, no un botón desactivado, impone la capacidad. Después elige Back to signup. La reserva guardada persiste aunque una actualización borre campos todavía no enviados.
Reduce el navegador o panel Studio a unos 390 píxeles e inspecciona también un ancho de tableta cercano a 768. No debe haber desplazamiento horizontal; etiquetas y ayuda deben seguir visibles y el par nombre/correo debe apilarse. Recorre los controles con Tab: todos necesitan una indicación visible al recibir el foco. Recupera un panel ancho y comprueba que el par puede usar dos columnas. No ocultes un campo difícil para conseguir que quepa.
Si todavía ves el diseño antiguo, comprueba que escribiste form.css en /home/labex/project/stillspoke, no base.css, y actualiza. Una ruta incorrecta no requiere reiniciar la VM. Autocomprobación: el visitante puede leer y manejar todo el formulario en anchos pequeños y grandes, la reserva real aparece en los registros y se conservan los visitantes. La comprobación lee tus registros reales y ejecuta una copia inalterada de tus archivos con datos temporales aislados; nunca envía tu formulario real por ti.
Resumen
Has asociado etiquetas visibles, restricciones nativas y ayuda con controles reales, hecho condicional la nota sin perder entradas y construido un diseño adaptable con foco de teclado visible. Utilizaste un ejemplo independiente, completaste una reserva real y contrastaste su confirmación con los registros aceptados. El siguiente laboratorio independiente construye el controlador de solicitudes y respuestas.
