Tienes una tabla con muchas columnas y, en la última, los botones de acciones (editar, eliminar, ver detalle). Para llegar a ellos hay que desplazarse en horizontal, la barra de desplazamiento no se ve y, si intentas fijar una columna con position: sticky, la celda no se queda pegada. Este es el patrón típico que produce el problema:
<div className="overflow-x-auto">
<Table>
<TableBody>
<TableRow>
{/* ...muchas celdas... */}
<TableCell className="sticky right-0 bg-background">
<Button size="sm">Editar</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
Este artículo explica por qué sticky se ancla al contenedor equivocado, cómo dejar un único contenedor con scroll, cómo fijar la columna con Tailwind y qué alternativas de interfaz existen. Los ejemplos usan Next.js, Tailwind CSS y el componente Table de shadcn/ui, pero la causa es CSS puro y aplica a cualquier tabla.
Por qué pasa
Según MDN, un elemento con position: sticky se mantiene fijo respecto al ancestro más cercano que tenga un "mecanismo de desplazamiento", es decir, un overflow con valor hidden, scroll, auto u overlay. Esto ocurre aunque ese ancestro no sea el que realmente se desplaza. Además, hace falta al menos un desplazamiento (top, right, bottom o left) distinto de auto en el eje correspondiente; sin él, sticky se comporta como relative.
El componente Table de shadcn/ui envuelve el <table> en un div con su propio overflow. En versiones anteriores del componente la clase es overflow-auto y en la versión actual del registro es overflow-x-auto. En ambos casos es un contenedor con scroll: cuando uno de los ejes de overflow no es visible, el otro eje también se comporta como auto.
Si además envuelves la tabla en otro overflow-x-auto, tienes dos contenedores anidados. Cuando el interno crece hasta el ancho de la tabla (por ejemplo, porque un elemento padre flex o grid sin min-w-0 se expande a su contenido), el que desborda es el externo o la propia página. La celda sticky sigue anclada al interno, que nunca se desplaza, así que no se pega a ningún borde. La barra horizontal tampoco se ve, porque vive en el contenedor que desborda: al final de la tabla o de la página, fuera de pantalla si hay muchas filas.
En una prueba mínima en Chromium con CSS equivalente, un contenedor externo de 600 px con scroll horizontal y un contenedor interno ensanchado dejó la celda con sticky right-0 fuera del área visible tras desplazar 300 px. Con un único contenedor, la misma celda quedó pegada al borde derecho. El resultado fue el mismo cuando el desbordamiento lo causaba la página por un hijo flex sin min-w-0.
Solución
-
Localiza qué contenedor hace scroll. En la consola del navegador, lista los ancestros de una celda que tienen mecanismo de desplazamiento:
const celda = document.querySelector("tbody td:last-child") let el = celda.parentElement while (el) { const { overflowX, overflowY } = getComputedStyle(el) if (/auto|scroll|hidden|overlay/.test(overflowX + overflowY)) { console.log(el, overflowX, overflowY, "desborda en horizontal:", el.scrollWidth > el.clientWidth) } el = el.parentElement }Si el primer elemento de la lista (el más cercano a la celda) imprime
falsey el desplazamiento horizontal ocurre en otro sitio, ya sea un contenedor más externo contrueo la propia página,stickyestá anclado al contenedor equivocado. -
Deja un único contenedor con scroll. Quita el
overflow-x-autode tu envoltorio y usa el que ya traeTable:// Antes: dos contenedores con scroll <div className="overflow-x-auto"> <Table>...</Table> </div> // Después: solo el contenedor de Table <Table>...</Table>Si la tabla está dentro de un elemento flex o grid, agrega
min-w-0a ese hijo para que no se expanda al ancho de la tabla:<main className="min-w-0 flex-1"> <Table>...</Table> </main> -
Fija la columna de acciones en el encabezado y en las celdas:
<TableHead className="sticky right-0 bg-background shadow-[-6px_0_6px_-6px_rgba(0,0,0,0.25)]"> Acciones </TableHead> <TableCell className="sticky right-0 bg-background shadow-[-6px_0_6px_-6px_rgba(0,0,0,0.25)]"> <Button size="sm" variant="outline">Editar</Button> </TableCell>stickyactiva el comportamiento yright-0es el umbral. El fondo opaco es obligatorio: sin él, el contenido que pasa por debajo se ve a través de la celda.bg-backgroundes un color del tema de shadcn/ui; en un proyecto de Tailwind sin ese tema la clase no existe y debes usar un color propio (por ejemplobg-white dark:bg-neutral-950). La sombra a la izquierda es opcional y deja claro que hay contenido debajo. -
Haz visible la barra y fija también el encabezado. La barra horizontal aparece en el borde inferior del contenedor. Si el contenedor mide lo que mide la tabla, queda al final de una lista larga. Limita su altura y la barra estará siempre a la vista; además, un encabezado con
sticky top-0funcionará dentro del mismo contenedor. Como los componentes de shadcn/ui se copian a tu proyecto, puedes agregar una propiedad para el contenedor encomponents/ui/table.tsx:function Table({ className, containerClassName, ...props }: React.ComponentProps<"table"> & { containerClassName?: string }) { return ( <div data-slot="table-container" className={cn("relative w-full overflow-x-auto", containerClassName)} > <table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} /> </div> ) }Y úsala así, con encabezados que se quedan arriba y la columna de acciones a la derecha:
<Table containerClassName="max-h-[70vh]"> <TableHeader> <TableRow> {/* ...columnas... */} <TableHead className="sticky top-0 right-0 z-30 bg-background">Acciones</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> {/* ...celdas... */} <TableCell className="sticky right-0 z-10 bg-background">...</TableCell> </TableRow> </TableBody> </Table>Los demás
TableHeadnecesitansticky top-0 z-20 bg-background. La jerarquía dez-index(30 para la esquina, 20 para el encabezado, 10 para la columna) evita que unas celdas tapen a otras. En los sistemas donde las barras de desplazamiento solo aparecen al desplazar, la columna fija y su sombra son la pista visual de que hay más contenido. -
Considera una alternativa de interfaz. Con muchas acciones por fila, agrúpalas en un menú "⋯" (en shadcn/ui,
DropdownMenu): la columna fija pasa a medir un solo botón. En móvil puedes mostrar tarjetas apiladas en lugar de la tabla. El prefijomd:de Tailwind aplica desde 768 px: un envoltorio conhidden md:blockoculta la tabla en pantallas pequeñas y otro conmd:hiddenoculta la lista de tarjetas en pantallas grandes.
Cómo verificar que quedó resuelto
- Vuelve a ejecutar el script del paso 1. Debe aparecer un único contenedor con
desborda en horizontal: true. - Desplaza la tabla en horizontal: las columnas intermedias deben pasar por debajo y la columna de acciones debe permanecer pegada al borde derecho del contenedor.
- Comprueba la posición por código. En la versión actual de shadcn/ui el contenedor tiene el atributo
data-slot="table-container"; en versiones anteriores usa otro selector:
const contenedor = document.querySelector('[data-slot="table-container"]')
const celda = document.querySelector("tbody tr:first-child td:last-child")
contenedor.scrollLeft = 300
const bordeDerecho = contenedor.getBoundingClientRect().left + contenedor.clientWidth
console.log(Math.abs(celda.getBoundingClientRect().right - bordeDerecho) <= 1) // true si está fijada
Por último, abre las herramientas de desarrollo con un ancho de 375 px y repite la prueba: la barra horizontal debe verse sin tener que bajar hasta el final de la página.
Variantes y errores relacionados
sticky top-0 no funciona dentro de una tarjeta con overflow-hidden
Una tarjeta con esquinas redondeadas suele llevar overflow-hidden. Ese valor crea un mecanismo de desplazamiento aunque nada se desplace allí, y el encabezado se ancla a la tarjeta en lugar de a la ventana. En una prueba con 80 filas y la página desplazada 400 px, el encabezado dentro de overflow: hidden quedó 399 px por encima del borde superior de la ventana, mientras que con overflow: clip se mantuvo pegado. En Tailwind, cambia overflow-hidden por overflow-clip. Según MDN, clip no crea un contenedor con scroll, pero tampoco permite el desplazamiento programático ni trae a la vista el contenido recortado cuando recibe el foco; úsalo solo si no hay elementos interactivos recortados.
Columna fija a la izquierda
Una primera columna con sticky left-0 tiene exactamente la misma causa y el mismo arreglo: un solo contenedor con scroll y un fondo opaco.
El color de la fila no llega a la celda fija
Como la celda fija tiene fondo propio, no cambia de color cuando la fila usa un estado de hover o de selección. Si lo notas, aplica a la celda el mismo color de ese estado.
Otro elemento con overflow entre la celda y el contenedor real
El mismo criterio vale para cualquier ancestro intermedio: si tiene overflow distinto de visible o clip, sticky se ancla a él. El script del paso 1 lo detecta.
Resumen
stickyse ancla al ancestro más cercano conoverflowdistinto devisible, aunque ese ancestro no sea el que se desplaza.- El
Tablede shadcn/ui ya trae su contenedor con scroll; envolverlo en otrooverflow-x-autodeja dos contenedores y rompesticky. - Deja un solo contenedor y agrega
min-w-0a los hijos flex o grid que lo contienen. - Fija la columna con
sticky right-0, fondo opaco y, si quieres, una sombra; limita la altura del contenedor para que la barra horizontal se vea. - Para encabezados dentro de una tarjeta redondeada, usa
overflow-clipen lugar deoverflow-hidden.
Comentarios
Publicar un comentario