Si tienes un blog en WordPress y te curras unos artículos de esos largos, llenos de información valiosa, seguro que alguna vez te has preguntado cómo facilitarles la vida a tus lectores para que no se pierdan entre tanto conocimiento.
¡Pues tengo buenas noticias! Hoy vamos a cocinar juntos una Tabla de Contenidos (TdC) dinámica, con estilazo y plegable, y lo mejor de todo: ¡sin instalar ni un solo plugin!
Antes de meternos en faena, déjame contarte por qué una TdC es una joya para tu blog:
- Mejora la experiencia de tus lectores (UX):
¿A quién le gusta perderse? Con una TdC, tus visitas pueden ver de un vistazo la estructura de tu post y saltar directamente a la sección que más les interesa. ¡Menos scroll, más felicidad! - Un pequeño empujón para tu SEO:
A Google le encanta el orden. Una TdC ayuda a los motores de búsqueda a entender mejor de qué va tu contenido y cómo está organizado. Esto puede incluso llevar a que Google muestre "enlaces de salto" o "sitelinks" directamente en los resultados de búsqueda, ¡haciendo tu artículo aún más atractivo! - Aspecto profesional:
Una TdC bien hecha le da un toque de profesionalidad y cuidado a tus contenidos.
Con este tutorial, vas a crear una TdC que se genera sola a partir de tus encabezados <h2> y <h3>, se puede plegar para no ocupar mucho espacio, y podrás personalizarla a tu gusto porque... ¡la vas a hacer tú!
Está pensado para ti, que te gusta tener el control, aprender cómo funciona WordPress por dentro y, por qué no, evitar llenar tu web de plugins innecesarios.
¿Qué necesitarás para crear tu Tabla de Contenidos? (Poca cosa, ¡prometido!)
Para seguir este tutorial, solo necesitas:
- Tu sitio WordPress listo para la acción.
- Un poquito de atrevimiento para copiar y pegar unos fragmentos de código. No te preocupes, te guiaré en cada paso como si estuviéramos tomando un café juntos.
- Unos 15-20 minutos de tu tiempo. ¡Verás qué rápido lo tenemos!
¡Venga, vamos al lío!
Paso 1: El esqueleto HTML – La base de nuestra tabla
Lo primero es poner los cimientos. Necesitamos un pequeño fragmento de código HTML que será el contenedor donde vivirá nuestra tabla de contenidos. Este código es súper sencillo:
<div id="tabla-de-contenidos-container" class="tabla-de-contenidos-wrap">
<h2 id="toc-title" tabindex="0" role="button" aria-expanded="false" aria-controls="toc-list">
Tabla de Contenidos
</h2>
<ul id="toc-list">
</ul>
</div>Lenguaje del código: PHP (php)¿Qué significa cada cosa?
<div id="tabla-de-contenidos-container" class="tabla-de-contenidos-wrap">: Es la caja principal que contendrá todo. Le damos unidpara identificarla con JavaScript y unaclasspara darle estilos con CSS.<h2 id="toc-title" ...>: Este será el título de nuestra tabla (puedes cambiar «Tabla de Contenidos» por lo que quieras, como «En este artículo encontrarás:»).
Los atributostabindex,role,aria-expandedyaria-controlsson para que sea accesible y funcione bien el mostrar/ocultar.<ul id="toc-list">: Esta es una lista desordenada (unaulde toda la vida) que de momento está vacía. Nuestro JavaScript la llenará mágicamente con los enlaces a tus encabezados.
¿Dónde lo pongo?
La forma más fácil es usando el editor de bloques de WordPress (Gutenberg):
- Edita la entrada o página donde quieras la TdC (o la plantilla, si usas un editor de sitios completo).
- Añade un nuevo bloque y busca el bloque "HTML personalizado".
- Pega el código HTML que te di arriba dentro de este bloque.
- Coloca este bloque donde quieras que aparezca la TdC, normalmente al principio de tu contenido, después de la introducción.

Paso 2: ¡A darle estilo! El CSS para una TdC chula y funcional
Ahora que tenemos el esqueleto, ¡vamos a ponerlo guapo!
Con el siguiente código CSS, le daremos un estilo minimalista, haremos que el título tenga un iconito [+] o [-] para mostrar/ocultar, y nos aseguraremos de que se vea bien.
Este es el código CSS completo. ¡No te asustes por la cantidad! Te explico las partes importantes más abajo.
/* CSS Completo para Tabla de Contenidos Estilo Minimalista
- COLAPSADA por defecto
- Funcionalidad de mostrar/ocultar (toggle)
- Título (H2) arriba, ocupando su propia línea y con texto a la izquierda
- Lista de enlaces (UL) centrada debajo del título cuando está abierta
- Texto de ítems de lista (LI) alineado a la izquierda
*/
/* Contenedor Principal de la Tabla de Contenidos */
.tabla-de-contenidos-wrap {
background-color: #ffffff;
border: none; /* Sin borde */
border-radius: 8px; /* Esquinas más redondeadas */
box-shadow: 0 4px 15px rgba(0,0,0,0.08); /* Sombra más sutil */
padding: 25px 30px;
margin-bottom: 25px;
text-align: center; /* Para centrar la lista ul#toc-list (que es inline-block) cuando está abierta */
}
/* Título de la Tabla de Contenidos (H2) */
.tabla-de-contenidos-wrap h2#toc-title {
font-family: 'Arial', sans-serif; /* O la fuente de tu tema */
font-weight: 600; /* Semi-negrita */
font-size: 1.1em;
color: #333333;
margin-top: 0;
margin-bottom: 0; /* Espacio vertical gestionado por el margin-top de ul#toc-list */
display: block; /* Asegura que el título ocupe su propia línea horizontalmente */
text-align: left; /* Mantiene el texto del título alineado a la izquierda dentro de su bloque */
cursor: pointer; /* Indica que es interactivo para el toggle */
position: relative; /* Necesario para posicionar el pseudo-elemento ::after (icono) */
padding-right: 25px; /* Espacio para el icono [+] / [-]. Ajusta si es necesario. */
}
/* Icono Indicador para el Toggle (en el título H2) */
.tabla-de-contenidos-wrap h2#toc-title::after {
content: '[+]'; /* Icono cuando la lista está CERRADA (por defecto y cuando aria-expanded="false") */
position: absolute;
right: 5px; /* Posición del icono desde la derecha del padding del h2 */
top: 50%;
transform: translateY(-50%);
font-size: 0.9em; /* Tamaño del icono, relativo al tamaño de fuente del h2 */
font-weight: normal; /* Para que el icono no sea negrita como el título */
color: #555555; /* Color del icono */
}
/* Icono cuando la lista está ABIERTA (aria-expanded="true") */
.tabla-de-contenidos-wrap h2#toc-title[aria-expanded="true"]::after {
content: '[-]';
}
/* Lista de Enlaces (UL) */
.tabla-de-contenidos-wrap ul#toc-list {
/* Estilos de formato base */
text-align: left; /* Asegura que el TEXTO DENTRO de la lista se alinee a la izquierda */
list-style: none; /* QUITA LOS MARCADORES DE LISTA (PUNTOS) */
padding-left: 0; /* Quita el padding izquierdo por defecto de las listas */
margin-bottom: 0; /* Sin margen inferior para la lista misma */
margin-top: 15px; /* Espacio entre el título y la lista CUANDO ESTÉ ABIERTA */
display: none; /* INICIALMENTE OCULTA */
/* Cuando se abra con .is-open, se volverá display: inline-block; para centrarse */
/* Opcional: Si quieres que la lista no sea más ancha que un cierto porcentaje cuando esté abierta */
/* max-width: 90%; */
}
/* Estado Abierto de la Lista de Enlaces */
.tabla-de-contenidos-wrap ul#toc-list.is-open {
display: inline-block; /* La hace visible Y permite que text-align:center del padre la centre */
}
/* Elementos Individuales de la Lista (LI) */
.tabla-de-contenidos-wrap ul#toc-list li {
margin-bottom: 10px; /* Espacio entre ítems de la lista */
line-height: 1.4; /* Altura de línea para mejorar legibilidad */
text-align: left; /* ASEGURA ALINEACIÓN IZQUIERDA DEL TEXTO DEL ÍTEM */
list-style-type: none; /* Refuerza quitar marcadores por si acaso, aunque debería heredar de UL */
}
/* Enlaces dentro de los Elementos de la Lista (A) */
.tabla-de-contenidos-wrap ul#toc-list li a {
font-family: 'Arial', sans-serif; /* O la fuente de tu tema */
font-size: 0.95em; /* Tamaño de fuente para los enlaces */
color: #555555; /* Color del texto del enlace */
text-decoration: none; /* Quita el subrayado por defecto de los enlaces */
transition: color 0.2s ease-in-out, padding-left 0.2s ease-in-out; /* Transición suave para hover */
display: inline; /* Asegura que el enlace no cause problemas de alineación de texto */
}
/* Hover de los Enlaces */
.tabla-de-contenidos-wrap ul#toc-list li a:hover {
color: #0073aa; /* Color de acento de WordPress al pasar el ratón */
padding-left: 5px; /* Pequeño desplazamiento del texto al pasar el ratón */
}
/* Estilo para Encabezados H3 dentro de la Lista */
.tabla-de-contenidos-wrap ul#toc-list li.es-h3 {
padding-left: 25px; /* Indentación para los elementos H3 */
}
/* Estilo para cuando la Tabla de Contenidos está vacía */
.tabla-de-contenidos-wrap.is-empty {
display: none; /* Si no hay ítems (controlado por JS), se oculta todo el contenedor */
}
Lenguaje del código: PHP (php)Un vistazo rápido a lo que hace este CSS:
.tabla-de-contenidos-wrap: Define la caja principal con un fondito blanco, esquinas redondeadas y una sombra suave. Eltext-align: center;es para ayudarnos a centrar la lista de enlaces cuando esté visible.h2#toc-title: Estiliza el título, le pone el cursor de "manita" para que se sepa que es clicable, y prepara el terreno para el iconito[+]/[-]. Eldisplay: block;ytext-align: left;aseguran que el título esté arriba y alineado a la izquierda.h2#toc-title::after: ¡Este es el truco para el icono! Usa un pseudo-elemento para mostrar[+]cuando la tabla está cerrada y[-]cuando está abierta ([aria-expanded="true"]).ul#toc-list: Aquí está la clave del comportamiento inicial:display: none;la mantiene oculta. También quitamos los estilos de lista (los puntitos) y los paddings feos.ul#toc-list.is-open: Cuando nuestro JavaScript añada la clase.is-opena la lista, esta regla la hará visible usandodisplay: inline-block;(lo que permite que eltext-align: center;del padre la centre como bloque) y le da unmargin-toppara separarla del título.- Lo demás son estilos para los elementos de la lista (
li), los enlaces (a) y un poquito de indentación para losh3.
¿Dónde va este CSS?
Tienes dos opciones principales, ¡elige la que más te guste!
- El Personalizador de WordPress: Ve a «Apariencia» → «Personalizar» → «CSS adicional». Pega todo el código CSS allí. Es fácil y rápido.
- En el archivo
style.cssde tu tema hijo: Si usas un tema hijo (lo cual es una práctica excelente), puedes añadir este código al final de su archivostyle.css.
Paso 3: La inteligencia – JavaScript para la dinámica y el «clic-clic»
Ahora toca darle el cerebro a nuestra TdC. Este script de JavaScript hará varias cosas importantes:
- Mirará dentro del contenido principal de tu artículo.
- Buscará todos los encabezados
<h2>y<h3>(que no hayas marcado para ignorar). - Creará un enlace para cada uno y lo añadirá a la lista
ul#toc-listque hicimos antes. - Añadirá unos
idúnicos a tus encabezados originales para que los enlaces de la TdC sepan a dónde saltar. - Y, por supuesto, ¡hará que el título sea clicable para mostrar y ocultar la lista!
Aquí tienes el código JavaScript:
<script>
document.addEventListener('DOMContentLoaded', function() {
// Selectores principales
const contentSelector = '.entry-content'; // ¡¡ATENCIÓN AQUÍ!! Mira abajo la explicación.
const tocContainerElement = document.querySelector('#tabla-de-contenidos-container');
const tocTitleElement = document.querySelector('h2#toc-title');
const tocListElement = document.querySelector('ul#toc-list');
// Verificar si los elementos esenciales de la TdC existen en el HTML
if (!tocContainerElement || !tocTitleElement || !tocListElement) {
if (tocContainerElement) {
tocContainerElement.classList.add('is-empty');
}
return;
}
// --- Inicio: Generación de la lista de contenidos ---
const contentArea = document.querySelector(contentSelector);
if (!contentArea) {
tocContainerElement.classList.add('is-empty');
return;
}
const headings = contentArea.querySelectorAll('h2:not(.ignore):not(#toc-title), h3:not(.ignore)');
let tocItemsCount = 0;
const slugCounts = {};
headings.forEach(function(heading) {
if (heading.closest('#tabla-de-contenidos-container')) {
return;
}
const text = heading.textContent.trim();
if (!text) return;
let slugBase = text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '');
let slug = slugBase;
if (slugCounts[slugBase]) {
slugCounts[slugBase]++;
slug = `${slugBase}-${slugCounts[slugBase]}`;
} else {
slugCounts[slugBase] = 1;
}
if (!heading.id) {
heading.id = slug;
} else {
slug = heading.id;
}
const listItem = document.createElement('li');
const link = document.createElement('a');
link.href = '#' + slug;
link.textContent = text;
listItem.appendChild(link);
if (heading.tagName.toLowerCase() === 'h3') {
listItem.classList.add('es-h3');
}
tocListElement.appendChild(listItem);
tocItemsCount++;
});
// --- Fin: Generación de la lista de contenidos ---
// Configurar la funcionalidad de mostrar/ocultar SOLO si se encontraron ítems
if (tocItemsCount > 0) {
tocContainerElement.classList.remove('is-empty');
function toggleTocList() {
const isExpanded = tocTitleElement.getAttribute('aria-expanded') === 'true';
tocTitleElement.setAttribute('aria-expanded', String(!isExpanded));
tocListElement.classList.toggle('is-open'); // Alterna la clase .is-open para mostrar/ocultar
}
tocTitleElement.addEventListener('click', toggleTocList);
tocTitleElement.addEventListener('keydown', function(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleTocList();
}
});
} else {
tocContainerElement.classList.add('is-empty');
}
});
</script>
Lenguaje del código: PHP (php)¡OJO! El Famoso contentSelector – La clave para que funcione en TU Tema:
Fíjate en la primera línea importante del script: const contentSelector = '.entry-content';.
- ¿Qué es esto? Es un selector CSS que le dice al JavaScript dónde buscar los encabezados
<h2>y<h3>para construir la tabla.'.entry-content'es una clase muy común que muchos temas de WordPress usan para el área principal del contenido de una entrada o página. - ¿Por qué es vital? Si tu tema usa una clase o ID diferente para el contenido principal, el script no encontrará tus encabezados y la tabla aparecerá vacía (o no aparecerá).
- ¿Cómo encontrar el selector de tu tema? ¡No te preocupes, es fácil!
- Ve a una de tus entradas de blog en el navegador.
- Haz clic derecho sobre el texto principal de tu artículo (no en el título del post, ni en la barra lateral, sino en el meollo del contenido).
- Selecciona «Inspeccionar» o «Inspeccionar elemento». Se abrirán las herramientas de desarrollador.
- Busca en el panel HTML que se abre el
<div>(o a veces<article>o<main>) que parece envolver todo tu contenido.
Suele tener una clase comoentry-content,post-content,single-post-content,td-post-content,article-content, etc. ¡Cada tema es un mundo! - Una vez que identifiques esa clase (o ID), cópiala.
Por ejemplo, si ves<div class="mi-clase-de-contenido">, tu selector será'.mi-clase-de-contenido'. Si ves<div id="contenido-principal">, tu selector será'#contenido-principal'. - Reemplaza
'.entry-content'en el script con el selector correcto de tu tema.


¿Y este script, dónde lo coloco?
- Opción sencilla (recomendada para empezar):
En el mismo bloque «HTML personalizado» donde pusiste el HTML de la TdC, pégalo justo debajo del código HTML, pero asegúrate de envolverlo entre etiquetas<script>y</script>como te he mostrado arriba. - Opción más ordenada (para usuarios un poco más avanzados):
Si te resulta cómodo, puedes guardar este código JavaScript en un archivo (por ejemplo,mi-tabla-contenidos.js) dentro de la carpeta de tu tema hijo (si usas uno).
Luego, necesitarías «encolar» ese script usando el archivofunctions.phpde tu tema hijo.
«Encolar» es la forma correcta en WordPress de añadir scripts y hojas de estilo.
Si esto te suena a chino, no te preocupes, la opción 1 funciona de maravilla.
Paso 4: «¡Tú no pasas!» – Excluyendo encabezados con la clase .ignore
Puede que tengas algún <h2> o <h3> que no quieres que aparezca en tu tabla (quizás un subtítulo decorativo o una llamada a la acción).
Simplemente, añade la clase ignore a ese bloque de encabezado en el editor de Gutenberg:
- Selecciona el bloque de Encabezado que quieres excluir.
- En la barra lateral derecha del editor, busca la sección «Avanzado».
- En el campo que dice «Clase(s) CSS adicional(es)», escribe la palabra
ignore.
¡Y listo! Ese encabezado será ignorado por el script.

Paso 5: ¡Hazla tuya! Ideas para personalizar tu TdC (opcional)
Lo genial de esta solución es que tienes control total. ¿Quieres cambiar los colores para que encajen con tu marca? ¿Usar otra fuente? ¿Quizás un icono diferente para el [+]/[-]?
¡Adelante! Solo tienes que modificar el código CSS que te di. Por ejemplo:
- Cambia los
background-color,color,border-radius,box-shadowen.tabla-de-contenidos-wrap. - Ajusta
font-family,font-size,colorenh2#toc-titleo en los enlacesul#toc-list li a. - Para los iconos, puedes usar otros caracteres en la propiedad
contentdeh2#toc-title::after(por ejemplo, flechas como'\25BC'▼ y'\25B2'▲, o incluso iconos SVG si te animas con CSS más avanzado).
¡El límite es tu imaginación (y un poquito de CSS)!
¡Y ya está! Tu Tabla de Contenidos funcionando a tope
Si has seguido todos los pasos, ¡enhorabuena! Ahora deberías tener una tabla de contenidos dinámica, plegable y con un estilo elegante en tus entradas. Tus lectores te lo agradecerán, y tú te sentirás como un auténtico crack de WordPress.

Solucionando pequeños contratiempos (FAQ rápido)
A veces, las cosas no salen a la primera. ¡No pasa nada! Aquí algunos problemillas comunes y cómo solucionarlos:
- Mi tabla no aparece o está vacía:
- El
contentSelectoren JavaScript: ¿Estás 100% seguro de que es el correcto para tu tema? Vuelve a revisarlo con el inspector del navegador. Es la causa número uno. - ¿Hay encabezados
<h2>o<h3>dentro de esa área de contenido y que no tengan la clase.ignore? - Errores de JavaScript: Abre la consola de tu navegador (normalmente con F12, pestaña "Consola"). ¿Ves algún mensaje en rojo? Eso te dará pistas.
- IDs en el HTML: Revisa que tu
div,h2yulde la TdC tengan losidcorrectos (tabla-de-contenidos-container,toc-title,toc-list).
- El
- El clic en el título no hace nada:
- De nuevo, la consola del navegador es tu mejor amiga. ¿Hay errores?
- Asegúrate de que el JavaScript se esté ejecutando después de que el HTML de la TdC esté cargado (el
DOMContentLoadeddel script debería encargarse de esto). - Verifica que el script está generando ítems (
tocItemsCount > 0), porque si no hay ítems, el clic se desactiva.
- Los estilos se ven raros o los puntos de la lista (
<li>) siguen apareciendo:- Caché: Limpia la caché de tu navegador y de cualquier plugin de caché que uses en WordPress. A veces los cambios CSS tardan en reflejarse.
- Especificidad CSS: Puede que tu tema tenga estilos más "fuertes" que están anulando los tuyos. Usa el inspector del navegador para ver qué estilos se están aplicando realmente a tu TdC. Si es necesario, puedes hacer tus selectores CSS un poco más específicos (ej.
body .mi-clase .tabla-de-contenidos-wrap ul#toc-list { list-style: none; }) o, como último recurso para un estilo muy terco, usar!important(ej.list-style: none !important;), aunque esto último úsalo con moderación. - Asegúrate de haber copiado todo el CSS y de que esté en el lugar correcto.
Descarga el ejemplo completo y pruébalo al instante
Para que puedas ver todo esto en acción sin complicaciones y experimentar por tu cuenta antes de integrarlo en tu WordPress, ¡te he preparado una sorpresa!
He empaquetado el esqueleto HTML, todos los estilos CSS que hemos visto y el código JavaScript funcional en un único archivo HTML, ¡listo para usar!
Este archivo también incluye un pequeño texto de ejemplo con varios encabezados <h2> y <h3> para que la tabla de contenidos se genere al instante y puedas probar cómo se despliega y se pliega.
Haz clic aquí para descargar el archivo de ejemplo:
Una vez descargado:
- Si has descargado el archivo
.zip, primero descomprímelo en tu ordenador. Encontrarás dentro el archivotabla-de-contenidos-ejemplo.html. - Busca ese archivo
tabla-de-contenidos-ejemplo.html. - Haz doble clic sobre él. Se abrirá en tu navegador web (Chrome, Firefox, Edge, el que uses).
¡Y ahí lo tendrás! Podrás ver la tabla de contenidos funcionando con el contenido de ejemplo. Esto es genial para jugar con el CSS, entender mejor cómo interactúa el JavaScript, o simplemente para ver el resultado final antes de ponerte manos a la obra en tu WordPress.
Importante: Recuerda que este archivo es un ejemplo autocontenido. Para implementarlo en tu sitio WordPress real, deberás seguir los pasos que te expliqué antesl (colocar el HTML de la TdC en tus entradas, el CSS en el Personalizador o
style.cssde tu tema hijo, y el JavaScript donde corresponda, ajustando elcontentSelectorpara tu tema).
Conclusión: ¡Navegación mejorada, lectores felices!
¡Felicidades por llegar hasta aquí y por darle este subidón de calidad a tus artículos!
Una tabla de contenidos bien implementada como la que acabas de crear no solo hace que tus publicaciones sean más fáciles de digerir para tus lectores, sino que también demuestra que te preocupas por su experiencia de usuario.
Ahora tienes una solución robusta, ligera y totalmente bajo tu control, ¡sin depender de plugins!
Y tú, ¿ya usas tablas de contenido en tus publicaciones? ¿Te animas a probar esta solución? ¿Qué otras funcionalidades te gustaría añadirle o qué te gustaría aprender a hacer con código en WordPress?
¡Cuéntamelo! Me encantará leerte y ayudarte en lo que pueda.
