Pular para o conteúdo
JavaScript

JavaScript: banner de cookies para LGPD e GDPR

Paloma Macetko
Um unicornio de crina luminosa atras de um balcao guardando um pote de biscoitos, e uma coruja mostrando duas plaquinhas de mesmo tamanho, uma de aceitar e outra de recusar

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:

orestbida/cookieconsentA biblioteca de consentimento de cookies usada neste artigo, na versão 3.0.1. Sem dependência nenhuma, licença MIT.github.com

"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 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:

Pagina de exemplo com o banner de cookies no canto inferior direito, com o titulo Controle sua privacidade e os botoes Aceitar e Rejeitar do mesmo tamanho, alem do botao Personalizar

🕵️ 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

JavaScript

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.