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>
);
}
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!