How to create a custom cookie banner for your React application
Recently I implemented a custom cookie banner solution on my Next.js site which you probably have seen a few seconds before. There are a lot of prebuilt cookie banners you can use for React or Next js sites but i wanted to create a custom cookie banner which also has some personal touch and keeps the design with the website in line. This took me quite some time because I had to figure out practices for opt-in, conditionally rendering the cookie banner dependent on the current page and some more features.
In this article I want to show you how I implemented and designed my cookie banner which hopefully will help you in creating your own custom cookie banner for your Next.js or any other React application.
Conditional rendering
What actually first happens is that a variable visible
with the default value false
is declared.
class CookieBanner extends Component { constructor(props) { super(props) this.state = { visible: false, } }
On every componentDidMount()
lifecycle method (of the CookieBanner component) it will be checked if the cookie consent is undefined
or debug
. If that condition is true that will mean a user is visiting the site for the first time and hasn't given cookie consent yet. To do so the visible variable will be changed to true and therefore the cookie banner will be visible. Also scrolling will be disabled so the user can't interact with any other element of the site.
In case the consent cookie already exists the visible variable will remain 'false' and the cookie banner will not show up.
componentDidMount() { const { debug } = this.props // if cookie undefined or debug if (Cookie.get("consent") === undefined || debug) { document.body.style.overflow = 'hidden'; this.setState({ visible: true }) } }
Now let's assume the cookie banner is visible and perhaps the user wants to check out the /privacy-policy or /site-notice pages to get some informations about the website. The problem now is that the cookie banner would be infront of these pages and hide the content. Therefore i used conditional rendering and the componentDidUpdate()
lifecycle method. The conditional rendering will check if this.state.visible
is not true or if the current page url includes 'privacy-policy' or 'site-notice'. In case one of these three conditions is true the cookie banner will not be rendered.
render() { if (!this.state.visible || window.location.href.includes("privacy-policy") || window.location.href.includes("site-notice")) { return null }
The componentDidUpdate()
lifecycle method will also check if the current page url includes 'privacy-policy' or 'site-notice' and enable scrolling again if one of these conditions is true. Now the cookie banner is not visible even though a user hasn't accepted or denied optional cookies yet. That means the user could navigate to any other page now and just scroll through the content which is not the way I planned it to be. That's the reason why here again it will be checked if the consent cookie exists and if it doesn't on any component update on the page the scrolling will be disabled again and the cookie banner will show up again.
componentDidUpdate() { const { debug } = this.props if (window.location.href.includes("privacy-policy") || window.location.href.includes("site-notice")) { document.body.style.overflow = 'scroll' } else if (Cookie.get("consent") === undefined || debug) { document.body.style.overflow = 'hidden'; } }
Now the user has to finally decied if she or he is fine with using (third-party) cookies. For that reason there will be some explanation in the cookie banner how the cookies are used and the user will find two buttons for 'Accept required and optional cookies' and 'Accept required cookies'. I decided to make the 'Accept required and optional cookies' more noticeable in comparison to the other option so the user is nudged into clicking the 'Accept required and optional cookies' button and allowing third-party cookies.
return ( <> <Background /> <CookieContainer> <CookieInnerContainer> <Wrapper> <CookieHeader> <Logo/> <Image src="/logos/android/android-launchericon-48-48.png" width="40" height="40" title="Max Dietrich" alt="Photo of Max Dietrich" className="profile u-photo" /> </CookieHeader> <CookieBody> <CookieBannerText> <CookieHeadline>Hi, welcome on mxd.codes 👋</CookieHeadline> <CookieContentContainer> <CookieContentBlock> You can easily support me by accepting cookies. These cookies will help with the following: <CookieTextList> <CookieTextItem> Collect audience interaction data and site statistics </CookieTextItem> <CookieTextItem> Deliver advertisements and measure the effectiveness of advertisements </CookieTextItem> <CookieTextItem> Show personalized content (depending on your settings) </CookieTextItem> </CookieTextList> </CookieContentBlock> <Text> <p> If you do not want to share your data with third parties but still want to support me you can do it via Paypal {' '} <TextLink href="/pay">mxd.codes/pay</TextLink> or follow me on my socials: <List> <Socialtem> <a href={config.socials.twitter} title="@mxdietrich on Twitter"> <FaTwitter /> </a> </Socialtem> <Socialtem> <a href={config.socials.instagram} title="_maxdietrich on Instagram" > <FaInstagram /> </a> </Socialtem> <Socialtem> <a href={config.socials.github} title="dietrichmax on GitHub"> <FaGithub /> </a> </Socialtem> <Socialtem> <a href={config.socials.strava} title="Max Dietrich on Strava"> <SiStrava /> </a> </Socialtem> <Socialtem> <a href={config.socials.xing} title="Max Dietrich on Xing" > <FaXing /> </a> </Socialtem> <Socialtem> <a href={config.socials.linkedin} title="Max Dietrich on Linkedin" > <FaLinkedin /> </a> </Socialtem> </List> </p> <p> For more information about cookies and how they are used please have a look at the Privacy Policy. </p> </Text> </CookieContentContainer> <Link href="/privacy-policy"> <CookieLink>Privacy Policy</CookieLink> </Link> <Link href="/site-notice"> <CookieLink>Site Notice</CookieLink> </Link> </CookieBannerText> <ButtonContainer> <Button onClick={() => { this.accept() }}>Accept required and optional cookies</Button> <Button onClick={() => { this.decline() }} backgroundColor="var(--content-bg)" color="#70757a" >Accept required cookies</Button> </ButtonContainer> </CookieBody> </Wrapper> </CookieInnerContainer> </CookieContainer> </> )
Setting Cookies
By clicking the 'Accept required and optional cookies' button the accept()
function will be triggered.
<Button onClick={() => { this.accept() }}>Accept required and optional cookies</Button>
The accept() function will
- set the consent cookie with the value true,
- enable Analytics,
- enable Adsense,
- track an (matomo) event,
- set the visible variable to false again and finally
- allow scrolling again.
accept = () => { Cookie.set("consent", true, { sameSite: "strict", expires: 365 }) enableGoogleAnalytics(); enableGoogleAdsense(); push(["trackEvent", "consent", "true"]) this.setState({ visible: false }) document.body.style.overflow = 'scroll' }
Cookies are created with help of the js-cookie.
The enableGoogleAnalytics()
and enableGoogleAdsense()
functions are stored separately because they will also be needed in the '_app.js' file, whichs wrapps the whole application.
The reason behind this is, that the analytics and ad scripts are just injected into the one page where the third-party cookies have been accepted by the user. But as soon as the user navigates to any other page after accepting third-party cookies the injected scripts are not exisiting in this page.
So inside the '_app.js' I also made use of the componentDidMount()
lifecycle method which will check on any component update on the page if the cookie consent' is true and inject the scripts again.
class MyApp extends App { componentDidMount() { if (window.location.href.includes(config.domain)) { if (Cookie.get("consent") === "true") { enableGoogleAnalytics(); enableGoogleAdsense(); } } } render() { ...
By clicking the 'Accept required cookies' button the 'decline()' function will be triggered.
<Button onClick={() => { this.decline() }} backgroundColor="var(--content-bg)" color="#70757a" >Accept required cookies</Button>
The decline()function will
- set the consent cookie with the value false,
- disable Analytics tracking if it has been enabled before
- track an event,
- set the visible variable to false again and finally
- allow scrolling again.
decline = () => { Cookie.set("consent", false, { sameSite: "strict", expires: 365 }) window['ga-disable-GA_MEASUREMENT_ID'] = true; push(["trackEvent", "consent", "false"]); this.setState({ visible: false }) document.body.style.overflow = 'scroll' }
And that's it. That's how I created a cookie banner with two options ("allow" and "deny" third-party cookies) which will be rendered conditionally depending on a consent cookie and depending on the current page the user is visiting. The CookieBanner component looks like the following but you can also find it in my Github repositiory https://github.com/dietrichmax/mxd-codes-frontend/blob/v2/src/components/cookies/cookie-banner.js.
import React, { Component } from "react" import Cookie from "js-cookie" import styled from "styled-components" import Link from "next/link" import media from "styled-media-query" import Image from "next/image" import Logo from "@/components/logo/logo" import { Button } from "@/styles/templates/button" import { FaGithub, FaTwitter, FaInstagram, FaLinkedin, FaXing, } from "react-icons/fa" //import { enableGoogleAnalytics } from "@/components/google-analytics/google-analytics" //import { enableGoogleAdsense } from "@/components/google-adsense/google-adsense" import { SiStrava } from "react-icons/si" import config from "src/data/internal/SiteConfig" import { push } from "@socialgouv/matomo-next" const Background = styled.div` position: fixed; z-index: 9997; right: 0; bottom: -200px; top: 0; left: 0; background-color: rgba(0, 0, 0, 0.5); ` const CookieContainer = styled.div` position: fixed; right: 0; bottom: 0; top: 0; left: 0; z-index: 9998; vertical-align: middle; white-space: nowrap; max-height: 100%; max-width: 100%; overflow-x: auto; overflow-y: auto; text-align: center; -webkit-tap-highlight-color: transparent; font-size: 14px; overflow-y: scroll; :after { content: ""; display: inline-block; height: 100%; vertical-align: middle; } ` const CookieInnerContainer = styled.div` width: var(--content-width); height: auto; max-width: none; border-radius: var(--border-radius); display: inline-block; z-index: 9999; background-color: var(--content-bg); text-align: left; white-space: normal; box-shadow: 0 2px 10px 0 rgb(0 0 0 / 20%); position: relative; border: 1px solid var(--body-bg); vertical-align: middle; ${media.lessThan("medium")` width: 90%; `} ` const Wrapper = styled.div` max-height: 100%; height: auto; max-width: none; text-align: center; border-radius: 16px; display: inline-block; text-align: left; white-space: normal; ` const CookieHeader = styled.div` padding: var(--space); display: flex; justify-content: space-between; ` const CookieBody = styled.div`` const CookieContentContainer = styled.div`` const CookieContentBlock = styled.div` margin-bottom: var(--space-sm); margin-top: var(--space); ` const CookieTextList = styled.ul` margin: 0; padding: 0; padding-inline-start: 1rem; ` const CookieTextItem = styled.li` margin: var(--space-sm) 0; ` const CookieBannerText = styled.div` padding: 0 var(--space); ` const CookieHeadline = styled.h1` text-align: center; font-size: 24px; font-weight: 400; margin-bottom: var(--space); ` const Text = styled.div` margin-bottom: var(--space-sm); margin-top: var(--space); ` const CookieLink = styled.a` border-bottom: 1px solid var(--text-color); :hover { border-bottom: none; } cursor: pointer; margin-right: var(--space-sm); ` const TextLink = styled.a` border-bottom: 1px solid var(--text-color); :hover { border-bottom: none; } ` const List = styled.ol` list-style: none; padding-inline-start: 0; display: flex; ` const Socialtem = styled.li` margin: var(--space-sm) var(--space-sm) var(--space-sm) 0; transition: 0.2s; :hover { color: var(--secondary-color); cursor: pointer; } ` const ButtonContainer = styled.div` margin: var(--space); display: flex; justify-content: space-between; ${media.lessThan("medium")` flex-direction: column; gap: var(--space-sm); `} ` class CookieBanner extends Component { constructor(props) { super(props) this.state = { visible: false, } } componentDidMount() { const { debug } = this.props // if cookie undefined or debug if (Cookie.get("consent") === undefined || debug) { document.body.style.overflow = "hidden" this.setState({ visible: true }) } } componentDidUpdate() { const { debug } = this.props if ( window.location.href.includes("privacy-policy") || window.location.href.includes("site-notice") ) { document.body.style.overflow = "scroll" } else if (Cookie.get("consent") === undefined || debug) { document.body.style.overflow = "hidden" } } accept = () => { Cookie.set("consent", true, { sameSite: "strict", expires: 365 }) //enableGoogleAnalytics(); //enableGoogleAdsense(); push(["trackEvent", "consent", "true"]) this.setState({ visible: false }) document.body.style.overflow = "scroll" } decline = () => { Cookie.set("consent", false, { sameSite: "strict", expires: 365 }) //window['ga-disable-GA_MEASUREMENT_ID'] = true; push(["trackEvent", "consent", "false"]) this.setState({ visible: false }) document.body.style.overflow = "scroll" } render() { if ( !this.state.visible || window.location.href.includes("privacy-policy") || window.location.href.includes("site-notice") ) { return null } return ( <> <Background /> <CookieContainer> <CookieInnerContainer> <Wrapper> <CookieHeader> <Logo /> <Image src="/logos/android/android-launchericon-48-48.png" width="40" height="40" title="Max Dietrich" alt="Photo of Max Dietrich" className="profile u-photo" /> </CookieHeader> <CookieBannerText> <CookieHeadline>Hi, welcome on mxd.codes 👋</CookieHeadline> <CookieContentBlock> You can easily support me by accepting optional (third-party) cookies. These cookies will help with the following: <CookieTextList> <CookieTextItem> Collect audience interaction data and site statistics </CookieTextItem> <CookieTextItem> Deliver advertisements and measure the effectiveness of advertisements </CookieTextItem> <CookieTextItem> Show personalized content (depending on your settings) </CookieTextItem> </CookieTextList> </CookieContentBlock> <Text> <p> If you do not want to share your data with third parties but still want to support please visit{" "} <TextLink href="/support">mxd.codes/support</TextLink> or message me on my socials: <List> <Socialtem> <a href={config.socials.twitter} title="@mxdietrich on Twitter" > <FaTwitter /> </a> </Socialtem> <Socialtem> <a href={config.socials.instagram} title="_maxdietrich on Instagram" > <FaInstagram /> </a> </Socialtem> <Socialtem> <a href={config.socials.github} title="mxdietrich on GitHub" > <FaGithub /> </a> </Socialtem> <Socialtem> <a href={config.socials.strava} title="Max Dietrich on Strava" > <SiStrava /> </a> </Socialtem> <Socialtem> <a href={config.socials.xing} title="Max Dietrich on Xing" > <FaXing /> </a> </Socialtem> <Socialtem> <a href={config.socials.linkedin} title="Max Dietrich on Linkedin" > <FaLinkedin /> </a> </Socialtem> </List> </p> <p> For more information about cookies and how they are used please have a look at the Privacy Policy. </p> </Text> <Link href="/privacy-policy"> <CookieLink>Privacy Policy</CookieLink> </Link> <Link href="/site-notice"> <CookieLink>Site Notice</CookieLink> </Link> </CookieBannerText> <ButtonContainer> <Button onClick={() => { this.accept() }} > Accept required and optional cookies </Button> <Button onClick={() => { this.decline() }} backgroundColor="var(--content-bg)" color="#70757a" > Accept required cookies </Button> </ButtonContainer> </Wrapper> </CookieInnerContainer> </CookieContainer> </> ) } } export default CookieBanner
If you also want to know how the previously mentioned enableGoogleAnalytics and enableGoogleAdsense() functions work keep reading.
Google Analytics in Next.js applications
The enableGoogleAnalytics functions consists of three functions.
addGoogleAnalytics()
initializeGoogleAnalytics()
trackGoogleAnalytics()
export function enableGoogleAnalytics () { addGoogleAnalytics().then((status) => { if (status) { initializeGoogleAnalytics() trackGoogleAnalytics() } }) }
First of all the Analytics script will be created and appended with the individual GA_TRACKING_ID
to the head-element.
export function addGoogleAnalytics () { return new Promise((resolve, reject) => { const head = document.getElementsByTagName('head')[0] const scriptElement = document.createElement(`script`) scriptElement.type = `text/javascript` scriptElement.async scriptElement.defer scriptElement.src = `https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}` scriptElement.onload = () => { resolve(true) } head.appendChild(scriptElement); }); }
After the script has been added to the site it needs to be initialized. I am also anonymizing IP adresses there and tracking a page view.
export function initializeGoogleAnalytics () { window.dataLayer = window.dataLayer || []; window.gtag = function(){window.dataLayer.push(arguments);} window.gtag('js', new Date()) window.gtag('config', process.env.NEXT_PUBLIC_GA_TRACKING_ID, { 'anonymize_ip': true, 'allow_google_signals': true }) const pagePath = location ? location.pathname + location.search + location.hash : undefined window.gtag(`event`, `page_view`, { page_path: pagePath }) }
To be able to also track a user changing pages we will use "next-router". It will track a page_view event everytime the route change has completed (a different page has been visited).
export function trackGoogleAnalytics () { Router.events.on('routeChangeComplete', (url) => { window.gtag(`event`, `page_view`, { page_path: url }) }); }
So by calling the function enableGoogleAnalytics () the Google Analytics Script will be added to the page, Google Analytics will be initalized and also all page changes will be tracked with it.
You also can have a look at https://github.com/dietrichmax/google-analytics-next which shows you how you can integrate Google Analytics in Nextjs.
Google Adsense in Next.js applications
The enableGoogleAdsense() function is similiar to the enableGoogleAnalytics () function. It will also create the default Google Adsense script and place it into the head of your react application.
export function enableGoogleAdsense () { const head = document.getElementsByTagName('head')[0] const scriptElement = document.createElement(`script`) scriptElement.type = `text/javascript` scriptElement.async scriptElement.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}` scriptElement.crossOrigin = "anonymous" head.appendChild(scriptElement); }
Afterwards you just need to place ad containers with the according client and slot id.
import styled from 'styled-components'; import { useEffect, useState } from 'react'; export function GoogleAdsenseContainer ( { client, slot }) { useEffect(() => { (window.adsbygoogle = window.adsbygoogle || []).push({}); }, []); const AdLabel = styled.span` font-size: 12px; ` return ( <div style={{textAlign: 'left',overflow: 'hidden'}} > <AdLabel>Advertisment</AdLabel> <ins className="adsbygoogle" style={{ display: "block" }} data-ad-client={client} data-ad-slot={slot} data-ad-format="auto" data-full-width-responsive="true" ></ins> </div> ); }
In case I missed some important information which you would add please let me know and if you liked the article feel free to share it.
First published September 23, 2022
Have you published a response to this? Send me a webmention by letting me know the URL.
Found no Webmentions yet. Be the first!
About The Author
Geospatial Developer
Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany. Support me