Posición CSS Sticky: Fijar cabecera de una tabla
Nuevo posicionamiento CSS Sticky
Los posicionamientos CSS que conocemos hace tiempo son static, relative, absolute y fixed. Ahora se introduce el nuevo posicionamiento sticky. El documento oficial W3C Sticky positioning está en fase de borrador (Working Draft) con fecha 01/09/2022. A la fecha en la que edito esta página (diciembre 2022) ya es soportado por todos los navegadores.
Un elemento con posición sticky es inicialmente tratado como static y posicionado en el flujo normal del documento, tras lo cual se aplica un desplazamiento a sus posiciones top, right, bottom y left de forma absoluta respecto al contenedor que supone un bloque de contención para ese elemento. Es una combinación de posicionamiento relativo y absoluto respecto a su bloque de contención. Las barras de desplazamiento (scroll) del contenedor no afecta al elemento, fijándose en esa posición independientemente de ese scroll.
Recordemos que el bloque de contención a efectos de posicionamientos static, relative o sticky es el ascendiente más cercano que es un elemento de bloque, con display
con valor block
o inline-block
por ejemplo. Para el posicionamiento absolute es el ascendiente más cercano que tiene posición distinta de static. Por último para posicionamiento fixed el bloque de contención es el viewport, que usualmente llamamos ventana del navegador.
En este tema recordaremos todos los posicionamientos con un ejemplo interactivo a continuación. En otro apartado veremos una aplicación práctica para sticky. Se tratará de fijar las filas y columnas cabeceras de una tabla.
Ejemplo: Posiciones CSS
inline
0px
0px
0px
0px
El ejemplo interactivo anterior tiene un bloque DIV identificado con id="container"
, con un texto en su interior. Resaltamos las palabras "rocín" y "flaco" en elementos STRONG, siendo el segundo identificado con id="box"
con un fondo de color. En lo que sigue nos referiremos a estos elementos como CONTAINER y BOX. En la Figura se observan estos elementos con posicionamiento static.
El flujo de texto posiciona el elemento BOX en la posición arriba e izquierda 38px 123px
, datos que presentamos en el ejemplo para observar como se desplaza realmente el elemento con cada posicionamiento. A BOX le damos estilos top, left, z-index, width
y height
. Todos esos valores serán ignorados para BOX, pues inicialmente es un elemento en línea (inline
) con posición estática (static
).
En la Figura se observa el posicionamiento relative para el elemento BOX, trasladándose 100px
hacia abajo y 100px
a la derecha, contando desde su posición inicial en el flujo del elemento. Es decir, se desplaza esas cantidades relativamente a su posición inicial, desde 38px 123px
hasta 138px 223px
. El espacio que inicialmente ocupaba BOX es preservado.
Si usamos el scroll del elemento CONTAINER veremos que el elemento BOX se mueve con el scroll. Con el posicionamiento relativo se ignoran las propiedades width
y height
que le hemos dado a BOX, pues es declarado como inline
. En el ejemplo existe la posibilidad de hacerlo inline-block
o block
, en cuyo caso si aplicarán esas propiedades.
En la Figura tenemos el posicionamiento sticky para el elemento BOX. En vertical se desplaza hasta la posición top 100px
. Vea que no se desplaza esta cantidad, sino a esa posición. Por lo tanto no es relativa a su posición inicial, sino absoluta respecto a CONTAINER. Sin embargo en horizontal no lo hace. Al ser un elemento inline
cuya posición horizontal izquierda inicial en el flujo era 123px
, mientras esa posición sea inferior a esa medida no se aplicará. En el ejemplo puede probar a aplicar una posición izquierda superior a 123px
para observar que entonces si lo aplica. También se aplicarán ambos si cambiamos BOX a inline-block
o block
. Vea que también se preserva el espacio inicial en el flujo de BOX.
El posicionamiento sticky de BOX funciona como una combinación de relative y absolute, a excepción de cuando actúamos sobre el scroll de CONTAINER, observándose que se queda fijo en la misma posición donde se desplazó. Esto será útil para fijar cabeceras de una tabla, como veremos en apartados posteriores.
En la Figura vemos el posicionamiento absolute para el elemento BOX. En primer lugar observamos que no se preserva el espacio inicial de BOX. De hecho no forma parte del flujo de texto del elemento CONTAINER. Por otro lado, aunque BOX es declarado inline
, el valor computado es block
, aplicándose las propiedades width
y height
.
En este ejemplo dimos posicionamiento relative a CONTAINER, con lo que BOX se desplaza a la posición 100px 100px
de forma absoluta respecto a CONTAINER. Si dejamos CONTAINER con posicionamiento static, entonces la ubicación sería 100px 100px
respecto a la ventana del navegador, comportándose como el posicionamiento fixed que veremos a continuación.
Recordar que antes dijimos que el bloque de contención para el posicionamiento absoluto era el ascendiente más cercano con posicionamiento distinto de static. Posicionar relative el elemento CONTAINER sin aplicar posiciones top, left, right o bottom no produce ningún efecto en su presentación, comportándose visualmente como si fuera static. Por eso usamos relative en un elemento padre cuando queremos posicionar absolute algún elemento hijo respecto a su padre.
Por último vemos en la Figura un posicionamiento fixed para BOX. Se ubica en la posición 100px 100px
de la ventana del navegador, permaneciendo fija cuando actuamos sobre el scroll de la ventana. Observe que también preserva el espacio inicial y se computa a block
, aplicándose ancho y alto a pesar de ser un elemento inline
.
Aplicación práctica de Sticky: fijar cabeceras de una tabla
Una aplicación práctica para sticky es fijar las filas o columnas cabeceras de una tabla. En la Figura vemos una tabla cuyo ancho y alto es mayor que el contenedor donde se ubica y donde hemos fijado la primera fila y la primera columna. Actuando sobre las barras de desplazamiento (scroll) del contenedor exterior veremos en el siguiente ejemplo interactivo que la primera fila permanece fija respecto al scroll vertical. Y que la primera columna permanece fija respecto al scroll horizontal.
Exponemos en este apartado el ejemplo y el CSS con notas explicativas necesarias para implementarlo. En siguientes apartados analizaremos características de las tablas que influyen sobre este comportamiento.
Ejemplo: Tabla con cabeceras fijas con sticky
Color | RGB | HEX | HSL | BG |
---|---|---|---|---|
black | 0,0,0 | #000000 | 0,0%,0% | 0 |
gray | 128,128,128 | #808080 | 0,0%,50.2% | 8421504 |
silver | 192,192,192 | #C0C0C0 | 0,0%,75.29% | 12632256 |
white | 255,255,255 | #FFFFFF | 0,0%,100% | 16777215 |
navy | 0,0,128 | #000080 | 240,100%,25.1% | 128 |
blue | 0,0,255 | #0000FF | 240,100%,50% | 255 |
aqua | 0,255,255 | #00FFFF | 180,100%,50% | 65535 |
green | 0,128,0 | #008000 | 120,100%,25.1% | 32768 |
teal | 0,128,128 | #008080 | 180,100%,25.1% | 32896 |
olive | 128,128,0 | #808000 | 60,100%,25.1% | 8421376 |
lime | 0,255,0 | #00FF00 | 120,100%,50% | 65280 |
yellow | 255,255,0 | #FFFF00 | 60,100%,50% | 16776960 |
maroon | 128,0,0 | #800000 | 0,100%,25.1% | 8388608 |
purple | 128,0,128 | #800080 | 300,100%,25.1% | 8388736 |
red | 255,0,0 | #FF0000 | 0,100%,50% | 16711680 |
orange | 255,165,0 | #FFA500 | 38.82,100%,50% | 16753920 |
fuchsia | 255,0,255 | #FF00FF | 300,100%,50% | 16711935 |
Este es el HTML del ejemplo, donde obviamos filas de la tabla para abreviar:
<div id="stickytab"> <table> <tr><th>Color</th><th>RGB</th><th>HEX</th>···</tr> ··· </table> </div>
Y el CSS para llevarlo a cabo es el siguiente, donde las notas indican lo mínimo imprescindible para su funcionamiento:
#stickytab { /* Outer container */ display: inline-block; /* (1) */ height: 20em; /* (1) */ width: 20em; /* (1) */ overflow: auto; /* (1) */ margin: 0.2em; min-height: 6em; min-width: 6em; resize: both; font-family: 'Arial', sans-serif; } #stickytab table { /* Table */ border-collapse: separate; /* (3) Default value */ border-spacing: 0; /* (3) */ } #stickytab table tr * { /* Cells */ border: black solid 1px; /* (3) */ border-top: none; /* (3) */ border-left: none; /* (3) */ padding: 0.2em; } #stickytab table tr:first-child * { /* Cells of the first row */ position: sticky; /* (2) */ top: 0; /* (2) */ z-index: 1; /* (2) */ border-top: black solid 1px; /* (3) */ background-color: gainsboro; /* (3) */ } #stickytab table tr *:first-child { /* Cells of the first column */ position: sticky; /* (2) */ left: 0; /* (2) */ z-index: 2; /* (2) */ border-left: black solid 1px; /* (3) */ background-color: white; /* (3) */ } #stickytab table tr:first-child *:first-child { /* Top left cell */ z-index: 3; /* (2) */ background-color: gainsboro; /* (3) */ }
Las anotaciones en el código anterior indican las características mínimas para que el ejemplo funcione como se espera:
- Contenedor exterior: Damos ancho y alto al contenedor exterior, siendo en este ejemplo las medidas de
20em
que no cubren todo el contenido de la tabla. Declaramos el contenedor como un bloque en línea (display: inline-block
), pues así la barra de scroll horizontal se dispone junto al bloque. Controlamos conoverflow: auto
el desbordamiento, para que se hagan visibles las barras de desplazamiento de forma automática cuando sean necesarias. - Posicionamiento: A las celdas de la primera fila y primera columna le damos
position: sticky
. Para completar este posicionamiento ubicamos arriba (top: 0
) las celdas de la primera fila. E izquierda las de la primera columna(left: 0)
. A la primera fila la ubicamos en una capa superior (z-index: 1
). Y a las de la primera columna en la siguiente capa (z-index: 2
). Por último la primera celda superior izquierda debe ir en otra capa superior (z-index: 3
). Con estas disposiciones de capas controlamos por donde se desplazan las celdas. - Bordes: La tabla ha de tener
border-collapse: separate
, valor por defecto que no sería necesario incluir en el CSS. El espaciado de bordes ha de ser cero (border-spacing: 0
). Todas las celdas tendrán un borde(border: black solid 1px)
, pero a continuación anulamos el borde superior e izquierdo. A las de la primera fila le ponemos el borde superior y a las de la primera columna el borde izquierdo. Es necesario dotar de colores de fondo (background-color
) para ocultar los contenidos de las celdas que se desplazan por debajo de otras.
En los siguientes apartados explicaremos algunas particularidades de una tabla para entender el motivo de usar el anterior CSS para lograr ese objetivo.
Ancho y alto de una tabla
Para entender la aplicación de sticky para fijar filas y columnas cabeceras de una tabla empezaremos por ver como responde una tabla a las propiedades ancho y alto. Usaremos el HTML siguiente, donde omitimos filas para abreviar:
<table id="tab1"> <tr><th>Color</th><th>RGB</th><th>HEX</th>···</tr> ··· </table>
El siguiente ejemplo interactivo presenta ese HTML donde podemos modificar las propiedades CSS table-layout, width, height y overflow:
Ejemplo: Ancho y alto de una tabla
#tab1 { table-layout: auto; /* default value */ width: auto; /* default value */ height: 10em; /* ignored */ overflow: visible; /* default value */ font-family: 'Arial', sans-serif; } #tab1 tr * { border: gray solid 1px; }
Color | RGB | HEX | HSL | BG |
---|---|---|---|---|
black | 0,0,0 | #000000 | 0,0%,0% | 0 |
gray | 128,128,128 | #808080 | 0,0%,50.2% | 8421504 |
silver | 192,192,192 | #C0C0C0 | 0,0%,75.29% | 12632256 |
white | 255,255,255 | #FFFFFF | 0,0%,100% | 16777215 |
navy | 0,0,128 | #000080 | 240,100%,25.1% | 128 |
blue | 0,0,255 | #0000FF | 240,100%,50% | 255 |
aqua | 0,255,255 | #00FFFF | 180,100%,50% | 65535 |
green | 0,128,0 | #008000 | 120,100%,25.1% | 32768 |
teal | 0,128,128 | #008080 | 180,100%,25.1% | 32896 |
olive | 128,128,0 | #808000 | 60,100%,25.1% | 8421376 |
lime | 0,255,0 | #00FF00 | 120,100%,50% | 65280 |
yellow | 255,255,0 | #FFFF00 | 60,100%,50% | 16776960 |
maroon | 128,0,0 | #800000 | 0,100%,25.1% | 8388608 |
purple | 128,0,128 | #800080 | 300,100%,25.1% | 8388736 |
red | 255,0,0 | #FF0000 | 0,100%,50% | 16711680 |
orange | 255,165,0 | #FFA500 | 38.82,100%,50% | 16753920 |
fuchsia | 255,0,255 | #FF00FF | 300,100%,50% | 16711935 |
En primer lugar hemos de decir que una tabla ignora la propiedad height
. La altura final se computa por la suma de alturas de sus filas, es decir, por el contenido de la tabla. Y en cuanto a overflow
sólo responde al desbordamiento horizontal con los valores visible
y hidden
, nada de scroll
. Así que no hay forma de activar barras de desplazamiento en una tabla fijando un ancho y alto inferior al contenido de la misma. Por eso en el ejemplo para fijar cabeceras la tabla se introduce en un contenedor cuyas dimensiones son inferiores a las de la tabla, lo que activa las barras de desplazamiento del contenedor exterior, no de la tabla.
La tabla (<table>
) dispone de la propiedad table-layout
y width
, ambas con valor por defecto auto
. Con width:auto
y cualquier valor de table-layout
, si no se especifican anchos de celdas (<td>
o <th>
) o columnas (<col>
), hará que el navegador calcule el ancho de la tabla partiendo de los contenidos de las celdas. Cada columna tomará el ancho del contenido de la celda con mayor ancho. En el ejemplo en su situación inicial vemos que el ancho de la primera columna se deduce de lo que ocupa la palabra "maroon".
Con table-layout:auto
y width:100%
, el navegador primero calcula el ancho de cada columna como antes, observando el ancho de la celda con contenido más largo en cada columna. Luego agranda los anchos de todas las columnas hasta ocupar el 100% del contenedor que alberga la tabla.
Con table-layout:fixed
y width:100%
se divide el ancho disponible del contenedor exterior por el número de columnas, adjudicando a cada columna el mismo ancho. Si algún contenido de una celda no cabe entonces será desbordado. En el siguiente apartado veremos como controlar el desbordamiento (overflow
) en las celdas.
Si especificamos un ancho para la tabla, el funcionamiento es el mismo que lo visto antes para 100% de ancho, sólo que ahora el ancho disponible no es el del contenedor exterior, sino el que hayamos declarado.
Como hemos dicho, la tabla responde a overflow
solo con los valores visible
(valor por defecto) y hidden
. Con hidden
podemos ocultar los contenidos de las celdas que se desborden por la parte derecha de la tabla. Usando en el ejemplo table-layout:fixed
y with:15em
observará que hay contenidos que se desbordan en las celdas, desbordes que podemos ocultar por la parte derecha de la tabla.
Bordes y desbordamientos en celdas de una tabla
Otro aspecto que necesitamos entender en el ejemplo de fijar cabeceras de una tabla con sticky tiene que ver con los bordes de la tabla. Usaremos el mismo HTML que en el ejemplo del apartado anterior:
<table id="tab2"> <tr><th>Color</th><th>RGB</th><th>HEX</th>···</tr> ··· </table>
En este ejemplo interactivo podemos modificar los bordes de la tabla. Y también el control del desbordamiento en las celdas. El desbordamiento no afecta al ejemplo con sticky y no vamos a entrar en detalles. Puede probar valores de overflow
, word-break
y overflow-wrap
(alias word-wrap
) para ver como actuan sobre el contenido de las celdas.
Ejemplo: Bordes y desbordamiento en celdas
#tab2 { table-layout: fixed; width: 20em; border-collapse: separate; /* default value */ border-spacing: 2px; /* default value */ border: red solid 1px; padding: 0.5em; font-family: 'Arial', sans-serif; } #tab2 tr * { border: gray solid 1px; overflow: visible; /* default value */ word-break: normal; /* default value */ overflow-wrap: normal; /* default value */ }
Color | RGB | HEX | HSL | BG |
---|---|---|---|---|
black | 0,0,0 | #000000 | 0,0%,0% | 0 |
gray | 128,128,128 | #808080 | 0,0%,50.2% | 8421504 |
silver | 192,192,192 | #C0C0C0 | 0,0%,75.29% | 12632256 |
white | 255,255,255 | #FFFFFF | 0,0%,100% | 16777215 |
navy | 0,0,128 | #000080 | 240,100%,25.1% | 128 |
blue | 0,0,255 | #0000FF | 240,100%,50% | 255 |
aqua | 0,255,255 | #00FFFF | 180,100%,50% | 65535 |
green | 0,128,0 | #008000 | 120,100%,25.1% | 32768 |
teal | 0,128,128 | #008080 | 180,100%,25.1% | 32896 |
olive | 128,128,0 | #808000 | 60,100%,25.1% | 8421376 |
lime | 0,255,0 | #00FF00 | 120,100%,50% | 65280 |
yellow | 255,255,0 | #FFFF00 | 60,100%,50% | 16776960 |
maroon | 128,0,0 | #800000 | 0,100%,25.1% | 8388608 |
purple | 128,0,128 | #800080 | 300,100%,25.1% | 8388736 |
red | 255,0,0 | #FF0000 | 0,100%,50% | 16711680 |
orange | 255,165,0 | #FFA500 | 38.82,100%,50% | 16753920 |
fuchsia | 255,0,255 | #FF00FF | 300,100%,50% | 16711935 |
En el ejemplo vemos que damos borde de 1px
a todas las celdas con el selector #tab2 tr *
. Mientras que a la tabla le hemos puesto un borde rojo también de 1px
, con un relleno (padding
) de 0.5em
.
Una tabla tiene la propiedad border-collapse
que afecta a los bordes con valores separate
(valor por defecto) y collapse
. Con el valor separate
los bordes de las celdas se separan el espacio indicado en border-spacing
, cuyo valor por defecto es de 2px
. Con el valor collapse
se ignora border-spacing
y los bordes de dos celdas contiguas collapsan en un único borde.
En la Figura vemos la tabla con los valores por defecto border-collapse: separate
y border-spacing: 2px
.
Si en una tabla con bordes separados ponemos border-spacing: 0px
veremos que los bordes contiguos se unen líneas de 2px
, como vemos en la Figura. No es la situación que deseamos para nuestro ejemplo con sticky, pues queremos bordes de 1px
.
Como observamos en la Figura, podemos usar el valor collapse
, colapsándose todos los bordes contiguos de las celdas. E incluso el borde exterior rojo de la tabla también se ha colapsado, desapareciendo junto al padding
de 0.5em
que le pusimos a la tabla. Tampoco es esta la situación que deseamos para nuestro ejemplo con sticky.
Además el uso de sticky en celdas con bordes colapsados tiene un efecto no deseado, como se observa en el siguiente ejemplo.
Ejemplo: Uso no deseado de Sticky en celdas con bordes colapsados
Color | RGB | HEX | HSL | BG |
---|---|---|---|---|
black | 0,0,0 | #000000 | 0,0%,0% | 0 |
gray | 128,128,128 | #808080 | 0,0%,50.2% | 8421504 |
silver | 192,192,192 | #C0C0C0 | 0,0%,75.29% | 12632256 |
white | 255,255,255 | #FFFFFF | 0,0%,100% | 16777215 |
navy | 0,0,128 | #000080 | 240,100%,25.1% | 128 |
blue | 0,0,255 | #0000FF | 240,100%,50% | 255 |
aqua | 0,255,255 | #00FFFF | 180,100%,50% | 65535 |
green | 0,128,0 | #008000 | 120,100%,25.1% | 32768 |
teal | 0,128,128 | #008080 | 180,100%,25.1% | 32896 |
olive | 128,128,0 | #808000 | 60,100%,25.1% | 8421376 |
lime | 0,255,0 | #00FF00 | 120,100%,50% | 65280 |
yellow | 255,255,0 | #FFFF00 | 60,100%,50% | 16776960 |
maroon | 128,0,0 | #800000 | 0,100%,25.1% | 8388608 |
purple | 128,0,128 | #800080 | 300,100%,25.1% | 8388736 |
red | 255,0,0 | #FF0000 | 0,100%,50% | 16711680 |
orange | 255,165,0 | #FFA500 | 38.82,100%,50% | 16753920 |
fuchsia | 255,0,255 | #FF00FF | 300,100%,50% | 16711935 |
El CSS es básicamente el mismo que el usado para el ejemplo con sticky. Solo que la tabla ahora tiene border-collapse
con valor collapse
. Y que se dibujan todos los bordes de todas las celdas. El efecto no deseado se observa en la Figura, donde vemos que al usar los scroll se fijan las celdas de la primera fila y primera columna, pero sus bordes desaparecen, pues los bordes colapsados no se desplazan con sticky.
Por eso hay que usar border-collpase: separate
con border-spacing: 0px
. Y declarar sólo los bordes derecho e inferior de todas las celdas. Y luego declarar el borde superior de la celdas de la primera fila a fijar. Y el borde izquierdo de las celdas de la primera columna a fijar.