Multi-select & Bulk Actions
Multi-select & Bulk Actions
8 reglasHeader con checkbox tristate: refleja la selección parcial con estado indeterminado
Cada fila seleccionable lleva un checkbox en la primera columna, y el checkbox del header funciona como control tristate: vacio (ninguna), indeterminado (algunas) y marcado (todas). El estado intermedio no es decorativo, es un requisito de WCAG 4.1.2 (Name, Role, Value): el estado del control de selección masiva debe comunicarse programaticamente. Para un <input type="checkbox"> nativo se usa la propiedad .indeterminate = true vía JS, no aria-checked="mixed" (el estado HTML del checkbox tiene prioridad sobre el ARIA). Para checkboxes ARIA custom (un div con role="checkbox") si se usa aria-checked="mixed". Un header binario que solo alterna marcado/vacio no puede representar la selección de 2 de 5 filas y deja al usuario sin saber el estado real.
Distingue "esta página" de "todos los registros" en dos pasos explicitos
Cuando una tabla página, marcar el header solo selecciona las filas visibles, no los miles de registros del backend. Si el usuario necesita actuar sobre el dataset completo, el sistema ofrece un segundo paso explicito: un banner o link que aparece solo después de seleccionar la página entera, con el conteo real ("Seleccionar los 340 resultados"). Shopify Polaris IndexTable expone esto con un paginatedSelectAllAction separado; Helios documenta tres niveles de alcance (global por bulk selection, página por header, fila por checkbox individual). Este patrón evita operaciones accidentales sobre todo el dataset cuando el usuario solo pretendia actuar sobre la página actual. Mostrar el ratio "3 de 340" ayuda al usuario a entender el alcance real.
Barra de acciones contextual sticky, con conteo y botón para limpiar
En cuanto el usuario selecciona un item, aparece una barra de acciones contextual flotante y fija al viewport que contiene tres cosas: el conteo ("3 seleccionados"), las acciones disponibles para ese conjunto, y un botón para limpiar la selección. Esta barra no reemplaza la toolbar global, es un layer superpuesto que aparece al seleccionar y desaparece al deseleccionar. Las acciones que no caben van a un menú "Más". Debe mantenerse visible durante el scroll para que en listas largas las acciones sigan accesibles sin volver arriba. La aparición puede animarse (entrada desde abajo) gateada por prefers-reduced-motion y con altura fija para no desplazar el contenido.
Shift-click para rango, hecho descubrible con hint visible o tooltip
Shift-click selecciona un rango contiguo: ancla en la primera fila marcada y extiende hasta donde cae el click con Shift. El equivalente de teclado es Shift+Espacio (selecciona la fila con foco) y Shift+Flecha (extiende la selección direccionalmente), según el APG. El problema critico de este patrón es su invisibilidad: quien no lo conoce nunca lo descubre y termina marcando 50 filas una por una. El sistema debe comunicarlo activamente: un tooltip en el primer checkbox ("Shift+click para seleccionar rango"), un hint al marcar la primera fila, o una instrucción en el empty state de la barra. Revelar el atajo sobre el elemento mismo es lo que lo hace practicamente accesible, no enterrarlo en documentación.
WAI-ARIA APG "Developing a Keyboard Interface" (Shift+Space, Shift+Arrow) · WAI-ARIA APG Grid Pattern · UXPin "Keyboard navigation patterns"Persiste la selección al scrollear y al paginar, o advierte antes de limpiarla
En tablas largas las filas marcadas salen del viewport al scrollear y el usuario pierde la referencia de que tiene seleccionado. La barra sticky con el conteo ("5 seleccionados") resuelve el scroll, pero la paginación exige una decisión explicita: si la selección se mantiene entre páginas, el conteo debe reflejar las páginas no visibles ("5 seleccionados en 2 páginas") y mostrarse como ratio cuando se pueda ("5 de 100"). Si la selección se limpia al paginar, el sistema debe advertirlo antes con un mensaje claro. Limpiar la selección en silencio al cambiar de página genera errores difíciles de detectar: el usuario ejecuta una acción creyendo que aplica a 10 filas cuando aplica a 0.
Helios Design System "Table multi-select" (selected count + ratio, scope global) · Eleken "Bulk action UX"Calibra la fricción según reversibilidad: modal para permanentes, undo-toast para reversibles
No toda acción en lote merece un modal de confirmación; la sobre-confirmación genera "dialog blindness" y el usuario confirma sin leer. La regla es proporcional al riesgo. Las acciones irreversibles (eliminar permanentemente) piden un modal que comunique las consecuencias y el número exacto de items afectados, con botón de variante peligrosa; GitLab Pajamas llega a exigir escribir el nombre del objeto para borrados de alta severidad. Las acciones reversibles (archivar, mover) pueden ejecutarse de inmediato y ofrecer "Deshacer" en un toast durante 5-10 segundos. Además, los botones destructivos se separan visualmente de los benignos: ponerlos juntos es uno de los errores top de diseño de aplicaciones según NN/g.
GitLab Pajamas "Destructive actions" · Eleken "Bulk action UX" (undo toast) · NN/g "Proximity of consequential options" & "Bulk actions guidelines"Progreso granular en ops largas: "124 de 500", sin congelar la UI
Las operaciones en lote sobre muchos registros pueden tardar segundos o minutos, y un spinner genérico "Procesando..." no informa nada y aumenta la ansiedad. El sistema muestra progreso concreto: procesados de total ("124 de 500"), estimación de tiempo restante cuando es posible, y estado por item cuando alguno falla. La UI permanece usable durante el proceso, sin bloquear al usuario. Al terminar se muestra un resumen accionable ("487 exitosos, 13 fallaron - Ver errores") con la opción de recuperar los fallidos. Como criterio: por encima de 2 segundos mostrar progreso, y por encima de 10 segundos ofrecer procesamiento en background con notificación al completar.
LogRocket "UI patterns for async workflows" ("124 of 500 rows", ciclo Queued→Running→Success→Failed) · Eleken "Bulk action UX" (feedback multinivel)Triada ARIA: aria-multiselectable en el grid, aria-selected por fila, conteo en live region
La accesibilidad de multi-select requiere tres capas ARIA coordinadas. (1) aria-multiselectable="true" en el elemento con role="grid" indica que permite selección multiple. (2) aria-selected="true/false" en cada fila o gridcell comunica su estado. (3) Un nodo con role="status" o aria-live="polite" anuncia el conteo actualizado cada vez que cambia la selección, para que el usuario de lector de pantalla sepa cuantos items tiene sin navegar a la toolbar. La region live debe estar vacia al cargar la página y poblarse por JS solo cuando hay algo que anunciar; polite retrasa el anuncio hasta que el lector termina la frase en curso. Marcar la fila solo con una clase CSS .selected sin aria-selected deja al usuario de teclado sin saber que tiene seleccionado.
<!-- 1. El grid permite selección multiple --> <table role="grid" aria-multiselectable="true"> <!-- 2. Cada fila declara su estado --> <tr aria-selected="true"> ... </tr> <tr aria-selected="false"> ... </tr> </table> <!-- 3. Live region: vacia al cargar, poblada por JS al cambiar selección --> <span role="status" aria-live="polite"> 3 filas seleccionadas </span>
<tr class="selected"> ... </tr> // solo color de fondo, sin ARIA
- R-664 Header con checkbox tristate: refleja la selección parcial con estado indeterminado
- R-665 Distingue "esta página" de "todos los registros" en dos pasos explicitos
- R-666 Barra de acciones contextual sticky, con conteo y botón para limpiar
- R-667 Shift-click para rango, hecho descubrible con hint visible o tooltip
- R-668 Persiste la selección al scrollear y al paginar, o advierte antes de limpiarla
- R-669 Calibra la fricción según reversibilidad: modal para permanentes, undo-toast para reversibles
- R-670 Progreso granular en ops largas: "124 de 500", sin congelar la UI
- R-671 Triada ARIA: aria-multiselectable en el grid, aria-selected por fila, conteo en live region