JavaScript
min read
July 10, 2024

How to create a custom snackbar in Next.js 14 using Tailwind CSS

How to create a custom snackbar in Next.js 14 using Tailwind CSS
Table of contents

Snackbars are an essential part of modern web applications. They provide quick, at-a-glance feedback to users, often informing them of the success or failure of an action.

However, when searching for libraries in React or Next.js to create snackbars, we often encounter heavy component libraries that can lead to performance issues or outdated libraries built with older versions of React.

In this blog post, we’ll walk through how to create a custom Snackbar component using Next.js 14, TypeScript, and Tailwind CSS that is customizable and lightweight.

Prerequisites

  • Node.js
  • Next.js 14
  • TypeScript
  • Tailwind CSS

Setting Up Next.js 14 with TypeScript and Tailwind CSS

First, let’s set up a new Next.js project with TypeScript. If you haven’t already done so, you can create a new Next.js project with the following commands:

npx create-next-app your-project-name --typescript
cd your-project-name

Install the necessary dependencies:

npm install classnames react-icons

classnames is is used for conditional class management. If you prefer, you can use the ternary operator to add conditional classes instead. react-icons is used to include the close icon inside the snackbar.

Now, let’s configure Tailwind CSS. Follow the official Tailwind CSS installation guide for Next.js.

Creating the Snackbar Component

We’ll start by creating a Snackbar component and its context. This component will be responsible for displaying messages to the user.

Step 1: Create a Context:

Create a new file called SnackbarContext.tsx in the context directory.

// src/context/SnackbarContext.tsx

import { createContext, ReactNode } from 'react';

// Define types for SnackbarContext and SnackbarProvider props
interface SnackbarContextType {
  (message: string, variant?: 'success' | 'error' | 'warning' | 'info'): void;
}

interface SnackbarProviderProps {
  children: ReactNode;
}

interface SnackbarState {
  show: boolean;
  message: string;
  variant: 'success' | 'error' | 'warning' | 'info';
}

const SnackbarContext = createContext<SnackbarContextType | undefined>(undefined);

Here, we define the types for the snackbar context and provider props. SnackbarContextType defines the shape of the context function, and SnackbarProviderProps establishes the shape of the provider's props. SnackbarState represents the state of the snackbar.

Step 2: Create the SnackbarProvider Component:

Define the SnackbarProvider component to provide the snackbar context to its children.

export const SnackbarProvider: React.FC<SnackbarProviderProps> = ({
  children,
}) => {
  const [snackbar, setSnackbar] = useState<SnackbarState>({
    show: false,
    message: '',
    variant: 'success',
  });

  const handleSnackbarClose = () => {
    setSnackbar((prev) => ({ ...prev, show: false }));
  };

  const showSnackbar = useCallback<SnackbarContextType>(
    (message, variant = 'success') => {
      setSnackbar({ show: true, message, variant });
    },
    []
  );

  return (
    <SnackbarContext.Provider value={showSnackbar}>
      {children}

      <div
        className={classNames(
          'transition-transform bottom-8 font-medium text-white left-8 fixed flex justify-between gap-2 items-center shadow-md min-h-[48px] max-w-[50vw] px-4 py-2 rounded-lg min-w-[300px] text-sm truncate whitespace-nowrap',
          {
            ['bg-successBg ']: snackbar?.variant === 'success',
            ['bg-errorBg ']: snackbar?.variant === 'error',
            ['bg-warningBg ']: snackbar?.variant === 'warning',
            ['bg-infoBg ']: snackbar?.variant === 'info',
            ['-translate-x-[200%]']: !snackbar?.show,
            ['translate-x-0']: snackbar?.show,
          }
        )}
      >
        {snackbar?.message}
        <div
          className="hover:bg-black/20 p-1 rounded-full cursor-pointer"
          onClick={handleSnackbarClose}
        >
          <IoMdClose size={20} />
        </div>
      </div>
    </SnackbarContext.Provider>
  );
};

In this component, we use useState to manage the snackbar state. The showSnackbar function displays the snackbar with a message and variant. The handleSnackbarClose function hides the snackbar.

Now, To automatically close the snackbar after some time, we’ll add a timeout. We can use setTimeout inside the showSnackbar function to trigger the close function after 5 seconds.

const SNACKBAR_TIMER = 5000;

// Set a new timer to hide the snackbar after 5 seconds
setTimeout(() => {
 handleSnackbarClose();
}, SNACKBAR_TIMER);

It will now close the snackbar after 5 seconds. However, if we open a second snackbar within those 5 seconds, the timer for the second snackbar won’t reset.

For example, if we open a success snackbar, the timer is set to close it in 5 seconds. If we trigger an error snackbar 3 seconds later, the snackbar content and variant will change, but it will close in the remaining 2 seconds.

To solve this issue, we use useRef to store the timer and clear it if another snackbar opens before the 5 seconds timeout. This ensures the timer resets each time a new snackbar is triggered. Here’s the updated code:

const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  
  const SNACKBAR_TIMER = 5000;

  const showSnackbar = useCallback<SnackbarContextType>(
    (message, variant = 'success') => {
      setSnackbar({ show: true, message, variant });

      // Clear the existing timer if it exists
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }

      // Set a new timer to hide the snackbar after 5 seconds
      timerRef.current = setTimeout(() => {
        handleSnackbarClose();
        timerRef.current = null;
      }, SNACKBAR_TIMER);
    },
    []
  );

  useEffect(() => {
    // Clean up the timer when the component unmounts
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    };
  }, []);

Step 3: Create a Hook to Use the Snackbar Context:

Define a hook to use the snackbar context in other components.

export const useSnackbar = (): SnackbarContextType => {
  const context = useContext(SnackbarContext);
  if (!context) {
    throw new Error('useSnackbar must be used within a SnackbarProvider');
  }
  return context;
};

This hook ensures that the context is used within a SnackbarProvider.

Here’s the final SnackbarContext.tsx file:

// src/context/SnackbarContext.tsx

'use client';

import classNames from 'classnames';
import {
  ReactNode,
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from 'react';
import { IoMdClose } from 'react-icons/io';

interface SnackbarContextType {
  (message: string, variant?: 'success' | 'error' | 'warning' | 'info'): void;
}

interface SnackbarProviderProps {
  children: ReactNode;
}

interface SnackbarState {
  show: boolean;
  message: string;
  variant: 'success' | 'error' | 'warning' | 'info';
}

const SnackbarContext = createContext<SnackbarContextType | undefined>(
  undefined
);

const SNACKBAR_TIMER = 5000;

export const SnackbarProvider: React.FC<SnackbarProviderProps> = ({
  children,
}) => {
  const [snackbar, setSnackbar] = useState<SnackbarState>({
    show: false,
    message: '',
    variant: 'success',
  });
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const handleSnackbarClose = () => {
    setSnackbar((prev) => ({ ...prev, show: false }));
  };

  const showSnackbar = useCallback<SnackbarContextType>(
    (message, variant = 'success') => {
      setSnackbar({ show: true, message, variant });

      // Clear the existing timer if it exists
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }

      // Set a new timer to hide the snackbar after 5 seconds
      timerRef.current = setTimeout(() => {
        handleSnackbarClose();
        timerRef.current = null;
      }, SNACKBAR_TIMER);
    },
    []
  );

  useEffect(() => {
    // Clean up the timer when the component unmounts
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    };
  }, []);

  return (
    <SnackbarContext.Provider value={showSnackbar}>
      {children}

      <div
        className={classNames(
          'transition-transform bottom-8 font-medium text-white left-8 fixed flex justify-between gap-2 items-center shadow-md min-h-[48px] max-w-[50vw] px-4 py-2 rounded-lg min-w-[300px] text-sm truncate whitespace-nowrap',
          {
            ['bg-successBg ']: snackbar?.variant === 'success',
            ['bg-errorBg ']: snackbar?.variant === 'error',
            ['bg-warningBg ']: snackbar?.variant === 'warning',
            ['bg-infoBg ']: snackbar?.variant === 'info',
            ['-translate-x-[200%]']: !snackbar?.show,
            ['translate-x-0']: snackbar?.show,
          }
        )}
      >
        {snackbar?.message}
        <div
          className="hover:bg-black/20 p-1 rounded-full cursor-pointer"
          onClick={handleSnackbarClose}
        >
          <IoMdClose size={20} />
        </div>
      </div>
    </SnackbarContext.Provider>
  );
};

export const useSnackbar = (): SnackbarContextType => {
  const context = useContext(SnackbarContext);
  if (!context) {
    throw new Error('useSnackbar must be used within a SnackbarProvider');
  }
  return context;
};

Using the Snackbar Context in Your Application

Now that we have our Snackbar context and provider, we can use it in our application. Wrap your application’s layout component with the SnackbarProvider like this:

// app/layout.tsx
import './globals.css';
import { SnackbarProvider } from '@/context/SnackbarContext';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <SnackbarProvider>
          {children}
        </SnackbarProvider>
      </body>
    </html>
  );
}

Adding Tailwind CSS Styles

In your tailwind.config.js file, add this line in the content array to apply the tailwind classes in snackbar component.

'./src/context/**/*.{js,ts,jsx,tsx,mdx}'

Add custom styles for the snackbar:

// tailwind.config.js

import type { Config } from 'tailwindcss';
const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
    './src/context/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        successBg: '#4caf50',
        errorBg: '#f44336',
        warningBg: '#ff9800',
        infoBg: '#2196f3',
      },
    },
  },
  plugins: [],
};
export default config;

Triggering the Snackbar

You can now use the useSnackbar hook to trigger the snackbar from any component. I'm using the code from this demo project as an example here:

// app/page.tsx

'use client';
import { useSnackbar } from '@/context/SnackbarContext';

export default function Home() {
  const showSnackbar = useSnackbar();

  const handleClick = (variant: 'success' | 'error' | 'warning' | 'info') => {
    showSnackbar(`This is a ${variant} message!`, variant);
  };

  return (
    <main className="p-10 min-h-screen text-center">
      <h1 className="mt-10 font-bold text-3xl xl:text-4xl">
        Custom Snackbar Demo
      </h1>
      <div className="flex flex-col justify-center items-center gap-6 px-5 sm:px-24 py-24 font-medium">
        <button
          className="bg-green-500 p-4 rounded-xl text-white"
          onClick={() => handleClick('success')}
        >
          Show Success Snackbar
        </button>
        <button
          className="bg-red-500 p-4 rounded-xl font-medium text-white"
          onClick={() => handleClick('error')}
        >
          Show Error Snackbar
        </button>
        <button
          className="bg-yellow-500 p-4 rounded-xl font-medium text-white"
          onClick={() => handleClick('warning')}
        >
          Show Warning Snackbar
        </button>
        <button
          className="bg-blue-500 p-4 rounded-xl font-medium text-white"
          onClick={() => handleClick('info')}
        >
          Show Info Snackbar
        </button>
      </div>
    </main>
  );
}

Custom Snackbar Demo

Conclusion

In this blog post, we’ve created a custom Snackbar component in a Next.js 14 application using TypeScript and Tailwind CSS. By following these steps, you can easily provide users with feedback on their actions, enhancing the overall user experience of your application.

Check out the production deployment here.

You can find the complete source code on my GitHub repo.

That’s it for this guide. See you next time, and if you found this guide helpful, please leave a like 👍. Happy coding!

Written by
Editor
No art workers.