How to create a custom cookie banner for your React application

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.

creating-your-own-custom-cookie-banner-with-privacy-in-mind.jpg

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.

Designing the Cookie Banner

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.

2022-05-01 15_40_35-How to create a custom cookie banner for your Next.js application • Max Dietrich.png

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>
      </>
    )

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.
Implementing Google Analytics in React 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.
Implementing Google Adsense in React 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.

About The Author

Max Dietrich
Max Dietrich

Geospatial Developer

Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany.

0 Webmentions

Have you published a response to this? Send me a webmention by letting me know the URL.

Found no Webmentions yet. Be the first!

Newsletter

Continue Reading

  • How to build a related posts component for your React blog

    Some blogs have these related articles or posts sections where visitors can have a preview at more content after they just read a post. That's what I wanted to create for my personal website which is built with React (Nextjs) and in this article I want to show you how you also can do it for any other react application. Continue reading...
  • How to deploy your Gatsby site on your own server

    With Gatsby 4 bringing in Server-Side Rendering (SSR) and Deferred Static Generation (DSG) you need an alternative methode to just hosting static files. Each page using SSR or DSG will be rendererd after a user requests it so there has be a server in the background which will handle these requests and build the pages if needed. Continue reading...
  • Fetching and storing activities from Garmin Connect with Strapi and visualizing them with NextJS

    Step-by-step guide explaining how to fetch data from Garmin Connect, store it in Strapi and visualize it with NextJS and React-Leaflet. Continue reading...