Estilos para listas y tablas en CSS
En esta lección
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ón | Qué hace |
|---|---|
counter-reset: nombre | Crea el contador (empieza en 0) |
counter-increment: nombre | Le 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. Ponpadding: 0al<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
paddingo fondo a::marker. No lo admite: usa::beforecon 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
backgrounda losth. - 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
| Propiedad | Para qué sirve | Ejemplo |
|---|---|---|
list-style-type | Tipo de viñeta o numeración | square, upper-roman, "✓ " |
list-style-position | Viñeta dentro o fuera | outside |
list-style: none | Quitar viñetas (menús) | Con margin: 0; padding: 0 |
::marker | Color y fuente del marcador | li::marker { color: red; } |
counter-reset / counter-increment | Crear y sumar un contador | counter-reset: paso |
counter() | Mostrar el contador | content: counter(paso) |
border-collapse | Juntar bordes de celdas | collapse |
border-spacing | Hueco entre celdas | 4px (con separate) |
caption-side | Posición del título | bottom |
nth-child(even) | Filas cebra | tbody tr:nth-child(even) |
font-variant-numeric | Cifras del mismo ancho | tabular-nums |
position: sticky | Cabecera fija | thead th { top: 0 } |
overflow-x: auto | Tabla con scroll en móvil | En 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).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.