JavaScript: banner de cookies para LGPD e GDPR
Olá meus Unicórnios! 🦄✨
Sabe aquela tarefa que parece ser meia hora de trabalho? 😅 "Colocar o banner de cookies no site." É só uma tarjinha embaixo com um botão "Aceitar", né? Foi mais ou menos isso que eu pensei — até parar para ler o que a lei realmente pede e perceber que o botão "Aceitar" sozinho é exatamente o jeito de fazer errado.
E tem uma coisa pior, que é a que quase ninguém repara: dá para colocar um banner lindo no site, com texto jurídico e tudo, e ele não servir para absolutamente nada. Porque enquanto o visitante lê o aviso e decide, o script de rastreio já rodou lá em cima, já gravou os cookies dele e já mandou os dados embora. O banner virou enfeite. 😳
Neste artigo eu implanto o banner num site simples — HTML, CSS e JavaScript, sem framework e sem build — usando a vanilla-cookieconsent, do Orest Bida. São três linhas no rodapé, um arquivo de configuração e um atributo em cada script que você quiser bloquear.
🍪 O que as duas leis pedem, na parte que vira código
Eu não sou advogada, e este artigo não é parecer jurídico — se o seu caso for delicado, fale com quem entende. Mas existe uma parte do assunto que é puramente técnica, e é dela que eu vou falar: o que muda no código por causa da LGPD (a lei brasileira) e do GDPR (a europeia).
Na prática, para quem está escrevendo o JavaScript, as duas convergem em quatro exigências:
- Consentimento prévio. O rastreio só pode começar depois do aceite. Não é "avise e continue rastreando".
- Recusar tem de ser tão fácil quanto aceitar. Um banner só com "Aceitar" não passa em nenhuma das duas.
- A escolha precisa ser regravável. Quem aceitou hoje pode recusar amanhã, e tem de conseguir fazer isso sozinho.
- Cookie necessário não pede permissão. O que faz o site funcionar (sessão, carrinho, segurança) está fora da regra.
A diferença que mais aparece no código é a granularidade: o GDPR é mais exigente em separar as finalidades (estatísticas, marketing, personalização) em opções independentes, e a LGPD trabalha com a mesma ideia de finalidade específica. É por isso que a configuração deste artigo tem categorias separadas em vez de um "aceita tudo ou nada".
📦 De onde vem a biblioteca
A biblioteca é a vanilla-cookieconsent, e ela mora aqui:
"Vanilla" aqui quer dizer o que parece: JavaScript puro, sem React, sem jQuery, sem dependência de coisa alguma. Você baixa o repositório e o que interessa são dois arquivos da pasta dist:
cookieconsent/dist/cookieconsent.umd.js
cookieconsent/dist/cookieconsent.css
E aqui vai uma opinião que eu defendo com carinho: sirva esses dois arquivos do seu próprio site, não de um CDN. Um banner de consentimento existe justamente para controlar o que sai do navegador do visitante — carregar a ferramenta de privacidade a partir de um domínio de terceiro, antes de qualquer consentimento, é uma ironia que dá para evitar copiando dois arquivos para dentro do projeto. 🙃
🔗 As três linhas que ligam tudo
No rodapé do site, logo antes de fechar o </body>, vão três linhas:
<link rel="stylesheet" href="/cookieconsent/dist/cookieconsent.css">
<link rel="stylesheet" href="/css.css">
<script type="module" src="/config.js"></script>
A primeira é o CSS da biblioteca, que traz o visual pronto do banner e do modal. A segunda é um CSS seu, e ela vem depois de propósito: é onde você ajusta o que quiser sem editar o arquivo da biblioteca — que um dia você vai querer atualizar. O meu tem quatro regras contadas, só para o banner não ficar estreito demais no desktop:
#cc-main .cm {
min-width: 360px;
}
@media screen and (max-width: 767px) {
#cc-main .cm {
min-width: auto;
}
}
A terceira linha é a que carrega a sua configuração — e repare no type="module", porque ele não é decoração.
É esse atributo que permite ao config.js começar com um import na primeira linha. Sem type="module", o navegador trata o arquivo como script comum, o import vira erro de sintaxe e nada roda — nem a biblioteca, nem o banner. E como o erro acontece antes da primeira instrução, você não vê nem um console.log seu aparecer. 😵
⚙️ O CookieConsent.run() e as categorias
O config.js abre importando a biblioteca e chamando run() com um objeto grande. Começando pelo topo:
import '/cookieconsent/dist/cookieconsent.umd.js';
CookieConsent.run({
guiOptions: {
consentModal: {
layout: "box inline",
position: "bottom right",
equalWeightButtons: true,
flipButtons: false
},
preferencesModal: {
layout: "box",
position: "right",
equalWeightButtons: true,
flipButtons: false
}
},
categories: {
necessary: {
readOnly: true
},
analytics: {},
marketing: {}
},
As categorias são o coração da configuração, e cada uma é uma chavinha que o visitante pode ligar e desligar:
necessary— o que faz o site funcionar: sessão, login, carrinho, segurança.analytics— medição de audiência, relatórios de comportamento.marketing— anúncios e remarketing.
Repare no readOnly: true da necessary. Ele faz a biblioteca desenhar aquela categoria como marcada e travada: o visitante vê que ela existe, vê a descrição, e não consegue desmarcar. Isso não é a biblioteca sendo autoritária — é a lei. Cookie necessário é aquele sem o qual o site não funciona, e ele está fora da exigência de consentimento justamente por isso. Se desse para recusar o cookie de sessão, o visitante perderia o login ao trocar de página. 😅
As outras duas categorias ficam com o objeto vazio ({}) mesmo. Vazio quer dizer "opcional, começa desligada", que é exatamente o que se quer.
👉 Os dois botões, do mesmo tamanho
Lá no guiOptions tem uma opção que parece estética e é requisito legal: o equalWeightButtons: true.
Ele faz "Aceitar" e "Rejeitar" saírem com o mesmo tamanho, a mesma cor e o mesmo contraste. Aquele padrão que a gente vê por aí — "Aceitar" num verde berrante e "Rejeitar" em cinza-clarinho, escondido num cantinho, às vezes nem botão sendo, só um link miudinho — é justamente o que as duas leis reprovam. Se recusar dá mais trabalho que aceitar, o consentimento não é livre.
O flipButtons: false só define a ordem em que os dois aparecem. E o layout: "box inline" com position: "bottom right" é o formato de caixinha no canto inferior direito, que é o que eu prefiro: aparece, dá para ler, e não cobre a página inteira.
🇧🇷 As traduções em pt_BR
A biblioteca não vem com textos em português — e nem deveria, porque esses textos são seus. Tudo o que aparece na tela sai do bloco language. Ele é longo, então vou mostrar a parte que interessa:
language: {
default: "pt_BR",
autoDetect: "browser",
translations: {
pt_BR: {
consentModal: {
title: "Controle sua privacidade",
description: "Nosso site usa cookies para melhorar a navegacao.",
acceptAllBtn: "Aceitar",
acceptNecessaryBtn: "Rejeitar",
showPreferencesBtn: "Personalizar",
footer: "<a href=\"/politica-de-privacidade/\">Politica de Privacidade</a>"
},
Os três botões do banner merecem nome a nome, porque os nomes deles contam o que fazem:
acceptAllBtn— aceita todas as categorias de uma vez.acceptNecessaryBtn— aceita só as necessárias, ou seja, recusa o resto. É o "Rejeitar".showPreferencesBtn— abre o modal para escolher categoria por categoria.
Esse acceptNecessaryBtn é o botão que faz o banner ser legal em vez de decorativo, e o nome dele na biblioteca é honesto: recusar não é "não gravar nada", é "gravar só o necessário".
O autoDetect: "browser" manda a biblioteca olhar o idioma do navegador; como só existe pt_BR nas traduções, o default atende todo mundo. Se um dia você acrescentar en, aí ele passa a escolher sozinho.
Depois vem o modal de preferências, com uma seção por categoria:
preferencesModal: {
title: "Quem pode usar seus cookies?",
acceptAllBtn: "Aceitar todos",
acceptNecessaryBtn: "Rejeitar todos",
savePreferencesBtn: "Salvar opcoes",
sections: [
{
title: "Necessarios <span class=\"pm__badge\">Sempre ativos</span>",
description: "Sao essenciais para o funcionamento do site. Sem eles o site nao funciona corretamente.",
linkedCategory: "necessary"
},
{
title: "Estatisticas",
description: "Traduzem as interacoes dos visitantes em relatorios de comportamento, de forma anonima.",
linkedCategory: "analytics"
},
{
title: "Marketing",
description: "Acompanham a navegacao dos visitantes para permitir anuncios mais relevantes.",
linkedCategory: "marketing"
}
]
}
O linkedCategory é o fio que amarra a seção à categoria lá de cima. Ele tem de bater exatamente com a chave que você escreveu em categories — e é aqui que mora um erro chato de achar: se você duplicar a seção de estatísticas para criar a de marketing e esquecer de trocar o linkedCategory, as duas seções passam a mexer na mesma chavinha. O visitante desliga "Marketing" e vê "Estatísticas" desligar junto, sem nenhum erro no console. 😳
Uma seção com linkedCategory vazio também é válida: vira texto explicativo, sem chavinha. É o lugar de escrever o parágrafo de apresentação do modal.
🚫 O erro que faz o banner inteiro não servir para nada
Se você só for ler um pedaço deste artigo, leia este. É o pulo do gato inteiro. 🙏
O jeito normal de colocar um script de estatísticas na página é este, e ele está errado para quem tem banner:
<script src="/estatisticas.js"></script>
Está errado porque o navegador executa esse script assim que chega nele — antes de o visitante ver o banner, antes de clicar em nada. Quando ele finalmente escolher "Rejeitar", o rastreio já aconteceu. Você tem o banner e não tem a conformidade.
A biblioteca resolve isso com um mecanismo próprio, e ele é lindo de simples: você diz ao navegador que aquilo não é JavaScript.
<script type="text/plain" data-category="analytics" data-src="/estatisticas.js"></script>
São três atributos, e cada um faz uma coisa:
O type="text/plain" é o bloqueio de verdade. Com esse tipo, o navegador olha a tag, decide que é texto qualquer e não executa. Não é esconder com CSS, não é adiar: é o navegador ignorando o conteúdo.
O data-category="analytics" é a etiqueta que amarra o script à categoria. Quando a categoria for aceita, a biblioteca procura todos os scripts com essa etiqueta e liberta cada um deles.
E o data-src no lugar do src é o detalhe cruel, o que pega quase todo mundo. Se você deixar src="/estatisticas.js" junto com o type="text/plain", o navegador realmente não executa o arquivo… mas baixa ele mesmo assim. E a requisição já é o rastreio: o servidor recebeu o IP, o user agent e o endereço da página de quem ainda não consentiu. Você bloqueou a execução e vazou o acesso. 😖
Quando o visitante aceita a categoria, a biblioteca não fica tentando "ligar" a tag antiga — ela cria um elemento <script> novo, copia os atributos, promove o data-src a src e põe no lugar do velho. É assim que tem de ser: trocar o type do elemento que já está na página não faz o navegador voltar atrás e executar. Ele já processou aquela tag e já decidiu ignorá-la.
🔁 Deixando mudar de ideia
A escolha tem de ser reversível — nas duas leis. E essa é a parte em que a biblioteca é generosa: você não escreve JavaScript nenhum. Basta um atributo num link do rodapé:
<a href="#" data-cc="show-preferencesModal">Preferencias de cookies</a>
A biblioteca varre a página procurando elementos com data-cc e pendura o clique sozinha. Sem addEventListener, sem getElementById, sem preventDefault. O visitante clica, o modal de preferências abre com as escolhas atuais já marcadas, ele muda o que quiser e salva.
Vale reservar um cantinho do rodapé para esse link em todas as páginas. Consentimento que não dá para revogar não é consentimento — e o modal que já existe no seu site resolve isso com uma linha de HTML.
🧩 A página inteira
Juntando tudo, o HTML mínimo de um site com o banner funcionando fica assim:
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Meu site</title>
<link rel="stylesheet" href="/cookieconsent/dist/cookieconsent.css">
<link rel="stylesheet" href="/css.css">
</head>
<body>
<h1>Meu site</h1>
<p>Conteudo normal da pagina.</p>
<footer>
<a href="#" data-cc="show-preferencesModal">Preferencias de cookies</a>
</footer>
<!-- Bloqueado ate haver consentimento da categoria analytics.
O endereco fica em data-src, e nao em src, senao o navegador
baixa o arquivo mesmo sem executar - e a requisicao ja e o rastreio. -->
<script type="text/plain" data-category="analytics" data-src="/estatisticas.js"></script>
<script type="module" src="/config.js"></script>
</body>
</html>
E o banner aparece assim, no canto inferior direito — repare nos dois botões do mesmo tamanho:
🕵️ O visitante invisível
Uma característica da biblioteca que vale conhecer antes que ela te assuste: existe uma opção hideFromBots, ligada por padrão, que esconde o banner de robôs.
A ideia faz sentido — o Google não precisa ver o aviso de cookies para indexar a sua página, e um banner na frente do conteúdo pode atrapalhar o rastreamento. Só que um dos sinais que ela usa para decidir se é robô é o navigator.webdriver, que fica ligado em navegador controlado por automação.
Ou seja: se você abrir o seu site com uma ferramenta de automação para conferir o banner, ele simplesmente não aparece — sem erro no console, sem aviso, sem nada. A página carrega perfeita, a biblioteca carrega perfeita, e o banner não vem. Dá para passar um tempo bom procurando o que quebrou na configuração antes de desconfiar que o comportamento é proposital. 🤯
💜 O que eu levo deste banner
O banner de cookies tem fama de ser tarefa chata de front-end, dessas que a gente resolve no automático copiando de algum lugar. E é aí que mora o problema: o copiado quase sempre tem só o botão "Aceitar" e quase nunca bloqueia script nenhum de verdade.
Com a biblioteca do Orest Bida, as quatro coisas que fazem o banner funcionar viram quatro detalhes pequenos e fáceis de conferir: o type="text/plain" com data-src para bloquear antes, o equalWeightButtons para os dois botões terem o mesmo peso, o readOnly na categoria necessária, e o data-cc="show-preferencesModal" para deixar um caminho de volta. É menos código do que parece — e é a diferença entre ter um banner e ter um banner que serve para alguma coisa.
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: populando uma planilha no Google Sheets
Como escrever dados numa planilha do Google Sheets com Node.js, e as tres armadilhas que transformam dado certo em planilha errada.
Node.js: consultando CPF e CNPJ no SPC
Como consultar CPF e CNPJ no SPC Brasil com Node.js puro: envelope SOAP na mao, sem biblioteca, e as armadilhas do caminho.
Testando câmera e microfone no navegador
Como pedir permissão, mostrar a prévia, gravar e reproduzir áudio e vídeo direto do navegador, com JavaScript puro e sem nenhuma biblioteca.