Abdessamad Ely Logo

Nextjs Cookie Consent Banner

Source code for the Nextjs Cookie Consent Banner project.

Abdessamad Ely
Abdessamad Ely
Software Engineer

CookieConsent.tsx

import Link from 'next/link'
import Cookies from 'js-cookie'
import { MouseEvent, useCallback, useEffect, useState } from 'react'
import { Container } from '../layouts/PublicLayout'

const USER_CONSENT_COOKIE_KEY = 'cookie_consent_is_true'
const USER_CONSENT_COOKIE_EXPIRE_DATE =
  new Date().getTime() + 365 * 24 * 60 * 60

const CookieConsent = () => {
  const [cookieConsentIsTrue, setCookieConsentIsTrue] = useState(true)

  useEffect(() => {
    const consentIsTrue = Cookies.get(USER_CONSENT_COOKIE_KEY) === 'true'
    setCookieConsentIsTrue(consentIsTrue)
  }, [])

  const onClick = (e: MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()

    if (!cookieConsentIsTrue) {
      Cookies.set(USER_CONSENT_COOKIE_KEY, 'true', {
        expires: USER_CONSENT_COOKIE_EXPIRE_DATE,
      })
      setCookieConsentIsTrue(true)
    }
  }

  if (cookieConsentIsTrue) {
    return null
  }

  return (
    <section className="fixed bottom-0 left-0 w-full py-2 md:py-4">
      <Container>
        <div className="flex flex-col items-start px-5 py-3 space-y-2 bg-gray-200 md:flex-row md:space-y-0 md:items-stretch md:space-x-2">
          <div className="flex items-center flex-grow text-gray-900">
            <p className="text-sm font-medium">
              This site uses services that uses cookies to deliver better
              experience and analyze traffic. You can learn more about the
              services we use at our{' '}
              <Link href="/privacy-policy">
                <a className="text-sm underline hover:text-lightAccent">
                  privacy policy
                </a>
              </Link>
              .
            </p>
          </div>
          <div className="flex items-center">
            <button
              className="p-3 text-sm font-bold text-white uppercase bg-gray-700 whitespace-nowrap"
              onClick={onClick}
            >
              Got it
            </button>
          </div>
        </div>
      </Container>
    </section>
  )
}

export default CookieConsent

PublicLayout.tsx

import Link from 'next/link'
import { ReactNode } from 'react'
import CookieConsent from '../banners/CookieConsent'

export const Container = ({ children }: { children: ReactNode }) => (
  <div className="w-full max-w-5xl px-2 mx-auto md:px-4">{children}</div>
)

const PublicLayout = ({ children }: { children: ReactNode }) => {
  return (
    <div className="flex flex-col min-h-screen">
      <header className="flex items-center border-b border-gray-200 h-14">
        <Container>
          <nav>
            <ul>
              <li>
                <Link href="/">
                  <a className="text-sm font-medium text-center capitalize">
                    Home
                  </a>
                </Link>
              </li>
            </ul>
          </nav>
        </Container>
      </header>
      <main className="flex-grow">
        <Container>{children}</Container>
      </main>
      <footer className="py-5 border-t border-gray-200">
        <Container>
          <nav className="my-5">
            <ul>
              <li>
                <Link href="/privacy-policy">
                  <a className="text-sm font-medium text-center capitalize">
                    Privacy Policy
                  </a>
                </Link>
              </li>
            </ul>
          </nav>
        </Container>
        <Container>
          <p className="text-sm font-medium text-center capitalize">
            copyright © {new Date().getFullYear()} all rights reserved
          </p>
        </Container>
      </footer>
      <CookieConsent />
    </div>
  )
}

export default PublicLayout

hello.ts

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

_app.tsx

import '../styles/globals.scss'
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp

_document.tsx

import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
} from "next/document";

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const originalRenderPage = ctx.renderPage;

    // Run the React rendering logic synchronously
    ctx.renderPage = async () => {
      return originalRenderPage({
        // Useful for wrapping the whole react tree
        enhanceApp: (App) =>
          function enhanceApp(props) {
            return <App {...props} />;
          },
        // Useful for wrapping in a per-page basis
        enhanceComponent: (Component) => Component,
      });
    };

    // Run the parent `getInitialProps`, it now includes the custom `renderPage`
    const initialProps = await Document.getInitialProps(ctx);

    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head>
          <meta charSet="utf-8" />
          <link rel="icon" type="image/svg+xml" href="favicon.svg" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

index.tsx

import Head from 'next/head'
import type { NextPage } from 'next'
import PublicLayout from '../components/layouts/PublicLayout'

const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Next.js cookie consent banner</title>
        <meta
          name="description"
          content="A Next.js cookie consent banner with TypeScript and Tailwind CSS."
        />
      </Head>

      <PublicLayout>
        <h1 className="text-3xl font-bold font-open">
          Next.js cookie consent banner
        </h1>
      </PublicLayout>
    </>
  )
}

export default Home

privacy-policy.tsx

import Head from 'next/head'
import type { NextPage } from 'next'
import PublicLayout from '../components/layouts/PublicLayout'

const PrivacyPolicy: NextPage = () => {
  return (
    <>
      <Head>
        <title>Our Privacy Policy</title>
        <meta name="description" content="Website privacy policy page" />
      </Head>

      <PublicLayout>
        <h1 className="text-3xl font-bold font-open">
          Website privacy policy page content
        </h1>
      </PublicLayout>
    </>
  )
}

export default PrivacyPolicy

globals.scss

@tailwind base;
@tailwind components;
@tailwind utilities;

.eslintrc.json

{
  "extends": "next/core-web-vitals"
}

.prettierrc

{
  "tabWidth": 2,
  "singleQuote": true,
  "trailingComma": "all",
  "semi": false
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
}

module.exports = nextConfig

package.json

{
  "name": "nextjs_cookie_consent_banner",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "js-cookie": "^3.0.1",
    "next": "12.2.3",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@types/js-cookie": "^3.0.2",
    "@types/node": "18.0.6",
    "@types/react": "18.0.15",
    "@types/react-dom": "18.0.6",
    "autoprefixer": "^10.4.7",
    "eslint": "8.20.0",
    "eslint-config-next": "12.2.3",
    "postcss": "^8.4.14",
    "sass": "^1.54.0",
    "tailwindcss": "^3.1.6",
    "typescript": "4.7.4"
  }
}

postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}