Saltar al contenido
listas-y-tablas-css.css · devschool

Estilos para listas y tablas en CSS

Lección 16 de 25 · 10 min de lectura · Actualizado el

En esta lección
  1. Listas: la propiedad list-style
  2. ::marker: estilo del marcador
  3. Quitar las viñetas: listas como menú
  4. Contadores CSS
  5. Tablas: lo básico
  6. Tablas fáciles de leer
  7. Tablas en el móvil
  8. Errores frecuentes
  9. Resumen

Las listas y las tablas aparecen en casi todas las webs: menús de navegación, pasos de un proceso, listas de características, tablas de precios, horarios o notas. Sin estilos se ven anticuadas y difíciles de leer. En esta lección aprenderás a personalizar las viñetas y los números de las listas, a convertir una lista en un menú y a diseñar tablas claras, con filas alternas, números bien alineados y una versión que funciona en el móvil.

Cómo se escriben en HTML (<ul>, <ol>, <table>, <thead>…) lo tienes en las lecciones de listas y tablas del curso de HTML.

Listas: la propiedad list-style

El tipo de marcador: list-style-type

ul { list-style-type: disc; }     /* círculo relleno (por defecto) */
ul.b { list-style-type: circle; } /* círculo hueco */
ul.c { list-style-type: square; } /* cuadrado */

ol { list-style-type: decimal; }        /* 1, 2, 3 (por defecto) */
ol.a { list-style-type: lower-alpha; }  /* a, b, c */
ol.b { list-style-type: upper-roman; }  /* I, II, III */
ol.c { list-style-type: decimal-leading-zero; } /* 01, 02, 03 */

ul.d { list-style-type: "✓ "; }   /* cualquier texto como viñeta */
ul.e { list-style-type: none; }   /* sin marcador */

Poder usar un texto como viñeta es muy práctico para listas de ventajas (”✓ ”) o de pasos (”→ ”).

La posición: list-style-position

ul { list-style-position: outside; } /* el marcador cuelga fuera del texto (por defecto) */
ul.dentro { list-style-position: inside; } /* el marcador forma parte de la primera línea */

Con outside, si un elemento ocupa varias líneas, todas quedan alineadas y la viñeta sobresale a la izquierda. Con inside, la segunda línea empieza debajo de la viñeta. Para textos largos, outside se lee mejor.

Una imagen como viñeta: list-style-image

ul.hojas { list-style-image: url("img/hoja.svg"); }

Existe, pero da poco control: no puedes cambiar el tamaño ni alinear bien la imagen. Hoy es más habitual usar ::marker o ::before, que verás a continuación.

El atajo

ul { list-style: square inside; }
ul.menu { list-style: none; }

::marker: estilo del marcador

El pseudoelemento ::marker selecciona la viñeta o el número de cada <li>. Con él puedes cambiar su color sin tocar el del texto:

li::marker {
  color: #2d6cdf;
  font-weight: 700;
}

ol.capitulos li::marker {
  content: "Cap. " counter(list-item) ": ";
}

::marker admite pocas propiedades: color, fuente (font-*), content y alguna más. No acepta padding, margin, fondos ni bordes. Si necesitas un marcador con fondo (un círculo de color, por ejemplo), usa ::before.

counter(list-item) es el contador que el navegador lleva automáticamente en toda lista: el número de cada elemento.

Quitar las viñetas: listas como menú

Un menú de navegación es una lista de enlaces, así que se escribe con <ul>. Para que no parezca una lista hay que quitar tres cosas:

.menu {
  list-style: none;   /* 1. las viñetas */
  margin: 0;          /* 2. el margen de arriba y abajo */
  padding: 0;         /* 3. la sangría de la izquierda */
}

¿Por qué el padding? Porque los navegadores añaden a <ul> y <ol> unos 40 px de padding-left para dejar sitio a la viñeta. Sin viñeta, ese hueco sobra.

Menú horizontal

<nav>
  <ul class="menu">
    <li><a href="/">Inicio</a></li>
    <li><a href="/cursos/">Cursos</a></li>
    <li><a href="/contacto/" aria-current="page">Contacto</a></li>
  </ul>
</nav>
.menu {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;        /* los <li> en fila */
  flex-wrap: wrap;      /* si no caben, bajan a otra línea */
  gap: 8px;
}

.menu a {
  display: block;       /* toda la zona es clicable, no solo el texto */
  padding: 8px 16px;
  color: inherit;
  text-decoration: none;
  border-radius: 6px;
}

.menu a:hover,
.menu a:focus-visible {
  background: #e8eefb;
}

.menu a[aria-current="page"] {
  background: #2d6cdf;
  color: white;
}

El padding va en el enlace, no en el <li>: así el área clicable es todo el botón y no solo las letras. aria-current="page" marca la página actual para los lectores de pantalla, y aprovechamos el mismo atributo para darle estilo. La colocación en fila se explica a fondo en Flexbox.

Contadores CSS

Los contadores son variables numéricas que CSS incrementa por ti. Sirven para numerar cosas que no son listas (secciones, figuras, preguntas) o para dar a los números un diseño propio.

Hacen falta tres piezas:

Propiedad o funciónQué hace
counter-reset: nombreCrea el contador (empieza en 0)
counter-increment: nombreLe suma 1 cada vez que aparece el elemento
counter(nombre)Escribe su valor, dentro de content
.pasos {
  list-style: none;
  padding: 0;
  counter-reset: paso;
}

.pasos li {
  counter-increment: paso;
}

.pasos li::before {
  content: counter(paso);
  display: inline-grid;
  place-items: center;
  width: 1.6em;
  height: 1.6em;
  margin-right: 8px;
  border-radius: 50%;
  background: #ffd166;
  font-weight: 700;
}

Resultado: cada paso lleva su número dentro de un círculo amarillo, algo imposible con ::marker.

Numeración de apartados

body { counter-reset: apartado; }
h2   { counter-increment: apartado; }
h2::before { content: counter(apartado) ". "; }
/* 1. Introducción   2. Instalación   3. Uso */

Los números son decorativos: no aparecen en el HTML. Si el número es importante para entender el contenido (como en unas instrucciones), usa un <ol> de verdad, que los lectores de pantalla anuncian correctamente.

Tablas: lo básico

border-collapse y border-spacing

Por defecto, cada celda tiene su propio borde y hay un pequeño hueco entre celdas. El resultado son bordes dobles:

table { border-collapse: separate; border-spacing: 2px; } /* valores por defecto */

Casi siempre querrás juntarlos en uno solo:

table {
  border-collapse: collapse;   /* un único borde entre celdas */
  width: 100%;
}
th, td {
  border-bottom: 1px solid #dde3ee;
  padding: 8px 12px;
}

border-spacing solo funciona con separate. Es útil si quieres celdas como “fichas” separadas, por ejemplo en un calendario: border-spacing: 4px.

Relleno y alineación

El padding de las celdas es lo que más mejora una tabla: sin él, el texto queda pegado a los bordes. Unos 8px 12px suelen bastar.

th { text-align: left; }            /* los <th> van centrados por defecto */
td, th { vertical-align: top; }     /* útil si hay celdas de varias líneas */

Números alineados a la derecha

Las cifras se comparan mejor alineadas a la derecha, porque así las unidades, decenas y centenas quedan en columna:

.num {
  text-align: right;
  font-variant-numeric: tabular-nums;  /* todas las cifras con el mismo ancho */
}
Sin alinear        Alineado a la derecha
1.250,00 €             1.250,00 €
87,50 €                   87,50 €
12.300,00 €           12.300,00 €

tabular-nums hace que el “1” ocupe lo mismo que el “8”. Muchas fuentes lo hacen por defecto, pero no todas.

caption-side

El <caption> es el título de la tabla. Por defecto va arriba; puedes llevarlo abajo:

caption {
  caption-side: bottom;
  padding: 8px;
  font-size: 0.85rem;
  color: #666;
}

Tablas fáciles de leer

Filas cebra con nth-child

Alternar el fondo de las filas ayuda a seguir una fila larga con la vista:

tbody tr:nth-child(even) {
  background: #f3f5f9;
}

even son las filas pares y odd las impares. Aplícalo sobre tbody tr para que la fila de cabecera no cuente. Tienes más sobre :nth-child en selectores.

Resaltar la fila al pasar el ratón

tbody tr:hover {
  background: #e8eefb;
}

Escríbelo después de la regla cebra: las dos tienen la misma especificidad, así que gana la última.

Cabecera fija con position: sticky

En tablas largas, la cabecera desaparece al hacer scroll y el usuario pierde de vista qué es cada columna. Con sticky se queda pegada arriba:

thead th {
  position: sticky;
  top: 0;
  background: #1b2d34;   /* imprescindible: sin fondo, las filas se ven a través */
  color: white;
}

Funciona tanto con el scroll de la página como con el de un contenedor con overflow. Si la web tiene una barra de navegación fija arriba, cambia top: 0 por su altura. Tienes sticky explicado en posicionamiento.

Tablas en el móvil

Una tabla con muchas columnas no cabe en 400 px. Si la encoges, las celdas se vuelven ilegibles. La solución más sencilla y fiable es envolverla en un contenedor con scroll horizontal:

<div class="tabla-scroll">
  <table>...</table>
</div>
.tabla-scroll {
  overflow-x: auto;          /* scroll horizontal solo si hace falta */
}
.tabla-scroll table {
  min-width: 600px;          /* por debajo, aparece el scroll */
}

La página no se desborda: solo la tabla se desplaza con el dedo. ¿Por qué un contenedor y no overflow en la propia tabla? Porque overflow no funciona bien sobre <table>; en un <div> sí.

Consejo: si quieres además la cabecera sticky dentro del contenedor, dale una altura máxima: max-height: 400px; overflow: auto;. Así el contenedor tiene scroll vertical propio y la cabecera se pega a su borde superior.

Otra técnica es convertir cada fila en una “tarjeta” en móvil con display: block, pero es más compleja y a veces confunde a los lectores de pantalla. Empieza siempre por el scroll horizontal.

Errores frecuentes

  • Quitar las viñetas y dejar el padding. El menú queda desplazado a la derecha. Pon padding: 0 al <ul>.
  • Usar <div> en vez de <ul> para los menús. Se pierde la información de “lista de N elementos” que dan los lectores de pantalla.
  • Intentar dar padding o fondo a ::marker. No lo admite: usa ::before con un contador.
  • Olvidar counter-reset. Sin él, el contador puede seguir contando desde otra lista.
  • Bordes dobles en tablas. Falta border-collapse: collapse.
  • Cabecera sticky transparente. Dale siempre un background a los th.
  • Cabecera sticky que no se pega. Algún antepasado tiene overflow: hidden, o el contenedor con scroll no tiene altura limitada.
  • Números centrados o a la izquierda. Las cifras se comparan mejor a la derecha.
  • Usar tablas para maquetar. Las tablas son para datos; para colocar cosas usa Grid o Flexbox.

Resumen

PropiedadPara qué sirveEjemplo
list-style-typeTipo de viñeta o numeraciónsquare, upper-roman, "✓ "
list-style-positionViñeta dentro o fueraoutside
list-style: noneQuitar viñetas (menús)Con margin: 0; padding: 0
::markerColor y fuente del marcadorli::marker { color: red; }
counter-reset / counter-incrementCrear y sumar un contadorcounter-reset: paso
counter()Mostrar el contadorcontent: counter(paso)
border-collapseJuntar bordes de celdascollapse
border-spacingHueco entre celdas4px (con separate)
caption-sidePosición del títulobottom
nth-child(even)Filas cebratbody tr:nth-child(even)
font-variant-numericCifras del mismo anchotabular-nums
position: stickyCabecera fijathead th { top: 0 }
overflow-x: autoTabla con scroll en móvilEn un <div> contenedor

Con listas y tablas resueltas, solo te queda uno de los elementos más difíciles de estilar: los formularios.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

listas-y-tablas-css.html
Resultado

Pon a prueba lo que has aprendido

[CSS] Has puesto list-style: none a un menú, pero los enlaces siguen desplazados unos 40 px a la derecha. ¿Qué falta?
.menu {
  list-style: none;
  display: flex;
}

[CSS] ¿Qué numeración muestran los <h2> de la página con este CSS?
body { counter-reset: apartado; }
h2 { counter-increment: apartado; }
h2::before { content: counter(apartado) ". "; }

[CSS] Aplicas una regla de filas cebra y otra de hover a las filas de una tabla, pero el hover no se nota en las filas pares. ¿Por qué?
tbody tr:hover { background: #e8eefb; }
tbody tr:nth-child(even) { background: #f3f5f9; }

[CSS] ¿Cuál es la forma más sencilla de que una tabla ancha no desborde la página en el móvil?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.