Cómo hacer un banner de cookies en HTML y JavaScript que cumpla
La respuesta corta
Un banner de cookies en HTML y JavaScript cumple si muestra «Aceptar» y «Rechazar» al mismo nivel en la primera capa, guarda la elección, no carga analítica ni publicidad hasta que hay consentimiento y deja cambiar de opinión en cualquier momento. Si usas etiquetas de Google, además debe fijar Consent Mode v2 en denegado por defecto y actualizarlo cuando el usuario elige.
Un banner hecho a mano puede cumplir igual que uno comercial. El problema rara vez es el diseño. Es lo que pasa detrás: scripts que se cargan antes de tiempo, un «Rechazar» escondido o una elección que no se recuerda. La norma es el artículo 22.2 de la LSSI, y los criterios, los de la Guía sobre el uso de las cookies de la AEPD en su versión de mayo de 2024. Aquí tienes cómo montar cada pieza con HTML y JavaScript sin librerías, un ejemplo de código comentado y los casos en los que no compensa mantenerlo tú. Qué debe decir el banner lo tienes en la guía del banner de cookies; aquí nos centramos en cómo programarlo.
¿Qué necesita un banner de cookies en HTML para cumplir?
Necesita seis piezas, y el aspecto visual es solo una de ellas:
- Primera capa con la información esencial y los botones «Aceptar», «Rechazar» y «Configurar».
- Segunda capa: la política de cookies y un panel por categorías.
- Memoria de la elección, para no preguntar en cada visita.
- Bloqueo previo: nada de analítica ni publicidad hasta que hay consentimiento.
- Señales para Google con Consent Mode v2, si usas sus etiquetas.
- Acceso permanente para cambiar de opinión.
Si falla la cuarta, el resto no sirve. El banner informa, pero las cookies ya están instaladas.
¿Cómo se estructuran la primera y la segunda capa?
La primera capa es el banner y la segunda, la política de cookies con el panel de configuración. La AEPD recomienda la información por capas para evitar la fatiga informativa: lo esencial al entrar y el detalle en una página aparte.
Primera capa
Según el apartado 3.1.2.2 de la guía, debe decir quién es el editor, para qué se usan las cookies, si hay de terceros y qué tipo de datos se recogen si se elaboran perfiles. También cómo aceptar, configurar y rechazar, con un enlace visible a la segunda capa y un texto como «Política de cookies». Tienes ejemplos de redacción en texto para el banner de cookies.
En HTML basta un contenedor con un título, un párrafo y tres botones. Usa el elemento button, no enlaces ni capas con un evento de clic: se manejan con teclado sin código extra.
Segunda capa y panel
El panel agrupa las cookies por finalidad, no una a una, y termina en un botón «Guardar selección». Guardar sin marcar nada equivale a rechazarlas todas. Las casillas empiezan desmarcadas: la AEPD no admite opciones premarcadas a favor de aceptar. Para la política de cookies puedes partir de nuestra plantilla de política de cookies.
¿Cómo pones «Aceptar» y «Rechazar» al mismo nivel?
Con el mismo elemento, el mismo estilo y en el mismo sitio. La guía pide un mecanismo de rechazo similar al de aceptación y en la misma capa: si aceptar es un botón, rechazar también. En código son tres reglas:
- Misma clase CSS para los dos botones: mismo tamaño, color y contraste.
- Los dos visibles en cualquier pantalla, sin desplegables ni scroll.
- Foco neutro. Al abrir el banner, lleva el foco al propio contenedor, no a «Aceptar».
«Configurar» puede tener otro estilo. La guía solo pide que sea claramente visible.
¿Dónde guardas la elección: cookie o localStorage?
En cualquiera de las dos. La guía de Consent Mode de Google cita ambas como forma de conservar la elección para las páginas siguientes. La diferencia es técnica:
| Cookie propia | localStorage | |
|---|---|---|
| La lee el servidor | Sí, viaja en cada petición | No, solo JavaScript |
| Entre subdominios | Sí, con el atributo Domain | No, va por origen |
| Caducidad | La fijas con Max-Age o Expires | No caduca: guarda la fecha y compruébala |
A efectos legales da igual. La LSSI se aplica a cualquier dispositivo de almacenamiento y recuperación de datos en el equipo del usuario. Importa qué guardas y durante cuánto tiempo.
Sobre la duración, la AEPD considera buena práctica que el consentimiento no dure más de 24 meses y que, mientras tanto, se conserve la elección sin preguntar en cada visita. Guarda también un número de versión. Si cambian las finalidades o los terceros, la guía exige actualizar la política y dejar decidir de nuevo: subes la versión y el banner vuelve a salir.
¿Necesita consentimiento esa cookie? La guía de la AEPD no lo dice de forma expresa. La CNIL francesa incluye entre las exentas las que conservan la elección del usuario. Limítala a eso. Lo ampliamos en cookies técnicas.
¿Cómo bloqueas la analítica y la publicidad hasta el consentimiento?
Sacándolas del flujo normal de ejecución. Un script con su tipo por defecto se ejecuta en cuanto el navegador lo lee, antes de que nadie pulse nada. Hay dos patrones.
Patrón text/plain con data-categoria
Cambias el tipo del script a text/plain y le añades la categoría en un atributo data-categoria. El navegador no ejecuta scripts cuyo tipo no reconoce como JavaScript, así que queda inerte. Cuando el usuario acepta esa categoría, tu código crea un script nuevo con el mismo contenido y lo inserta. Cambiar el atributo type del script existente no basta: el navegador no lo vuelve a evaluar.
Carga dinámica
La otra vía es no poner el script en el HTML y crearlo con document.createElement('script') solo tras el consentimiento. Encaja con etiquetas de un único archivo externo. El primer patrón es más cómodo cuando hay bloques en línea, como la configuración de gtag.
Ninguno de los dos cubre lo que no pasa por tu código: iframes de vídeo o de mapas, píxeles pegados en otras plantillas o scripts que añade un plugin. Revísalos también.
¿Cómo integras Consent Mode v2?
Con dos comandos de gtag: uno por defecto, con todo denegado, y otro de actualización cuando el usuario elige. La ayuda de Google Ads indica que, si mantienes tu propio banner, tienes que integrar su API de consentimiento tú mismo. Según la documentación para desarrolladores:
- El comando
defaultva en todas las páginas y antes de cualquier comando que envíe datos, comoconfigoevent. wait_for_updatefija cuántos milisegundos esperan las etiquetas a la actualización. Sirve si el banner carga de forma asíncrona.- La elección debe persistir, en una cookie propia o en localStorage, para fijar el estado correcto en las páginas siguientes.
La correspondencia habitual es analítica con analytics_storage, y marketing con ad_storage, ad_user_data y ad_personalization. En el ejemplo, además, las etiquetas de Google no cargan hasta que hay consentimiento, el enfoque que la guía de Consent Mode v2 describe como modo básico. Si prefieres el avanzado, lee antes modo básico vs avanzado.
¿Cómo queda el código completo?
Así queda un banner mínimo con dos categorías, analítica y marketing. Cambia G-XXXXXXX por tu ID de Google Analytics y adapta los textos. Primero, en el head, antes de cualquier etiqueta de Google:
<script>
// Cola de gtag: guarda los comandos aunque gtag.js aún no haya cargado
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Consent Mode v2: todo denegado hasta que el usuario decida
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});
</script>
<!-- Etiquetas bloqueadas: text/plain no se ejecuta; data-categoria dice cuándo activarlas -->
<script type="text/plain" data-categoria="analitica"
data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script type="text/plain" data-categoria="analitica">
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
</script>
Después, el banner y la lógica, antes de cerrar el body:
<style>
/* Aceptar y Rechazar comparten clase: mismo peso visual */
.cmb-boton { padding: .6em 1.2em; font: inherit; color: #fff; background: #1f4e8c; border: 0; }
[hidden] { display: none !important; }
</style>
<div id="cmb-banner" role="dialog" aria-labelledby="cmb-titulo"
aria-describedby="cmb-texto" tabindex="-1" hidden>
<h2 id="cmb-titulo">Cookies en esta web</h2>
<p id="cmb-texto">
En Ejemplo S.L. usamos cookies propias y de terceros para medir las visitas
y para mostrarte publicidad según un perfil hecho con las páginas que visitas.
Puedes aceptarlas, rechazarlas o elegir por categorías.
<a href="/politica-de-cookies/">Política de cookies</a>
</p>
<button type="button" class="cmb-boton" data-accion="aceptar">Aceptar</button>
<button type="button" class="cmb-boton" data-accion="rechazar">Rechazar</button>
<button type="button" data-accion="configurar"
aria-expanded="false" aria-controls="cmb-panel">Configurar</button>
<!-- Panel por categorías: casillas sin marcar -->
<form id="cmb-panel" hidden>
<fieldset>
<legend>Elige qué cookies aceptas</legend>
<label><input type="checkbox" checked disabled> Necesarias (siempre activas)</label>
<label><input type="checkbox" name="analitica"> Analítica</label>
<label><input type="checkbox" name="marketing"> Marketing y publicidad</label>
</fieldset>
<button type="submit">Guardar selección</button>
</form>
</div>
<!-- Acceso permanente para cambiar de opinión -->
<button type="button" id="cmb-reabrir">Configurar cookies</button>
<script>
(function () {
var CLAVE = 'cmb_consentimiento';
var VERSION = 1; // súbela si cambian finalidades o terceros
var VIGENCIA = 365 * 24 * 60 * 60 * 1000; // 12 meses; la AEPD aconseja no pasar de 24
var banner = document.getElementById('cmb-banner');
var panel = document.getElementById('cmb-panel');
var configurar = banner.querySelector('[data-accion="configurar"]');
var focoAnterior = null;
// Devuelve las categorías guardadas, o null si no hay, caducaron o son de otra versión
function leer() {
try {
var d = JSON.parse(localStorage.getItem(CLAVE));
if (!d || d.version !== VERSION || Date.now() - d.fecha > VIGENCIA) return null;
return d.categorias;
} catch (e) { return null; }
}
function guardar(cat) {
try {
localStorage.setItem(CLAVE, JSON.stringify({ version: VERSION, fecha: Date.now(), categorias: cat }));
} catch (e) { /* almacenamiento bloqueado: se volverá a preguntar */ }
}
// Traduce las categorías del banner a las cuatro señales de Consent Mode v2
function avisarAGoogle(cat) {
var ads = cat.marketing ? 'granted' : 'denied';
gtag('consent', 'update', {
'analytics_storage': cat.analitica ? 'granted' : 'denied',
'ad_storage': ads,
'ad_user_data': ads,
'ad_personalization': ads
});
}
// Sustituye cada script bloqueado de una categoría aceptada por uno ejecutable
function activar(cat) {
document.querySelectorAll('script[type="text/plain"][data-categoria]').forEach(function (viejo) {
if (!cat[viejo.dataset.categoria]) return;
var nuevo = document.createElement('script');
if (viejo.dataset.src) { nuevo.src = viejo.dataset.src; nuevo.async = true; }
else { nuevo.text = viejo.text; }
viejo.replaceWith(nuevo); // el original desaparece: no se activa dos veces
});
}
function abrir() {
focoAnterior = document.activeElement;
banner.hidden = false;
banner.focus(); // el foco entra en el banner, no en «Aceptar»
}
function cerrar() {
banner.hidden = true;
panel.hidden = true;
configurar.setAttribute('aria-expanded', 'false');
if (focoAnterior && focoAnterior.focus) focoAnterior.focus(); // devuelve el foco
}
function decidir(cat) {
var antes = leer();
guardar(cat);
avisarAGoogle(cat);
cerrar();
// Si retira algo que había aceptado, recarga: lo ya ejecutado no se puede descargar
if (antes && ((antes.analitica && !cat.analitica) || (antes.marketing && !cat.marketing))) {
location.reload();
} else {
activar(cat);
}
}
banner.addEventListener('click', function (e) {
var accion = e.target.getAttribute('data-accion');
if (accion === 'aceptar') decidir({ analitica: true, marketing: true });
if (accion === 'rechazar') decidir({ analitica: false, marketing: false });
if (accion === 'configurar') {
panel.hidden = !panel.hidden;
configurar.setAttribute('aria-expanded', String(!panel.hidden));
if (!panel.hidden) panel.elements.analitica.focus();
}
});
// Guardar sin marcar nada equivale a rechazar
panel.addEventListener('submit', function (e) {
e.preventDefault();
decidir({ analitica: panel.elements.analitica.checked, marketing: panel.elements.marketing.checked });
});
// Escape cierra el panel, no el banner: cerrar sin elegir no es consentir
banner.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) {
panel.hidden = true;
configurar.setAttribute('aria-expanded', 'false');
configurar.focus();
}
});
// Reabre el banner con la elección actual marcada
document.getElementById('cmb-reabrir').addEventListener('click', function () {
var cat = leer() || {};
panel.elements.analitica.checked = !!cat.analitica;
panel.elements.marketing.checked = !!cat.marketing;
abrir();
});
// En cada página: si hay elección válida se aplica sin preguntar; si no, sale el banner
var guardado = leer();
if (guardado) { avisarAGoogle(guardado); activar(guardado); } else { abrir(); }
})();
</script>
Para producción faltan los estilos completos, la versión móvil y borrar las cookies propias de analítica cuando el usuario retira el consentimiento. Las de terceros no se pueden borrar desde tu dominio. Pruébalo todo en una ventana de incógnito antes de publicar.
¿Cómo haces el banner accesible?
Con botones nativos, un foco bien gestionado y todo usable con teclado. La guía de patrones ARIA del W3C para diálogos da las pautas:
- Rol y nombre. El contenedor lleva
role="dialog"yaria-labelledbyapuntando a un título visible. - Foco al abrir. Pasa a un elemento del diálogo. En el ejemplo, al propio contenedor, para no empujar hacia ninguna opción.
- Foco al cerrar. Vuelve al elemento que lo abrió.
- Teclado. Tab y Mayúsculas+Tab recorren los controles en orden. Si lo haces modal, añade
aria-modal="true"y evita que el foco salga del diálogo. - Escape con cuidado. En un diálogo modal cierra, pero aquí solo cierra el panel. Cerrar el banner sin elegir no debe tratarse como consentimiento: la AEPD recuerda que la inactividad no lo es.
¿Cuándo sale más a cuenta una CMP?
Cuando la web no es tuya, o cuando son varias. El código de arriba funciona el día que lo publicas. El trabajo viene después:
- Registro de consentimientos. La política de consentimiento de usuarios de la UE de Google pide conservar registros del consentimiento. Con localStorage, la única prueba está en el navegador del visitante.
- Mantenimiento. Cada etiqueta nueva, cada cambio de Google o de la AEPD te obliga a tocar código en cada web.
- Escaneo. Alguien tiene que revisar qué cookies instala de verdad la web. Un plugin o un iframe nuevo cambian la lista sin avisar.
- Coherencia. La segunda capa debe coincidir con lo que la web instala.
Si prefieres no mantenerlo tú, Cookies Marca Blanca trae «Aceptar» y «Rechazar» en la primera capa, configuración por categorías, Consent Mode v2 con todo denegado por defecto y bloqueo de lo que gestionas desde él, sin mostrar nuestra marca. Se instala con un fragmento, como explica instalar el banner en una web HTML. Si ya tienes un banner propio y quieres saber si bloquea lo que debe, la auditoría gratuita de cookies lo comprueba. Para una web sencilla con Analytics, el código de este artículo es un buen punto de partida; en una cartera de clientes, lo caro es mantenerlo.
Preguntas frecuentes
¿Puedo guardar la elección en localStorage en vez de en una cookie?
Sí. La LSSI se aplica a cualquier dispositivo que almacene y recupere datos en el equipo del usuario, no solo a las cookies, y Google cita las dos opciones para conservar el estado de Consent Mode entre páginas. Con localStorage tienes que guardar tú la fecha y comprobar la caducidad.
¿La cookie que guarda la respuesta del banner necesita consentimiento?
La guía de la AEPD no lo dice de forma expresa, pero pide recordar la elección para no preguntar en cada visita, y la CNIL francesa incluye entre las exentas las que conservan la elección del usuario. Debe limitarse a guardar esa elección.
¿Cuánto tiempo puedo recordar la elección del usuario?
La AEPD considera buena práctica que el consentimiento no dure más de 24 meses y que, mientras tanto, la web conserve la elección sin volver a preguntar. Si cambian las finalidades o los terceros, hay que actualizar la política y dejar decidir de nuevo.
¿Basta con ocultar el banner cuando el usuario pulsa «Rechazar»?
No. Lo que cuenta es que los scripts de analítica y publicidad no se ejecuten. Un script normal pegado en el HTML se ejecuta al cargar la página, antes de que nadie pulse nada, así que hay que bloquearlo hasta que haya consentimiento.
Fuentes
- Guía sobre el uso de las cookies, mayo de 2024 (AEPD)
- Ley 34/2002, de servicios de la sociedad de la información (LSSI), art. 22.2 (BOE)
- Set up consent mode on websites (Google for Developers)
- Set up your consent banner with a consent management platform or a content management system (Ayuda de Google Ads)
- EU user consent policy (Google)
- Dialog (Modal) Pattern, ARIA Authoring Practices Guide (W3C)
- Cookies et traceurs : que dit la loi ? (CNIL)