Notification
Clear user feedback is essential for a smooth user experience. In this article, we’ll build a reusable Notification component in Next.js to show success, error, or warning messages like “Account created” or “Something went wrong.” Perfect for sign-in, sign-up, and other user actions.
1. Setup codes
1.1 Create context and hook
Create a file libs/contexts/NotificationContext.tsx
jsx"use client"; import { createContext, ReactNode, useState } from "react"; export type NotificationStatus = "success" | "error" | "warning" | null; type NotificationContextType = { status: NotificationStatus; title: string; message?: string; actions: { openNotification: (status: NotificationStatus, message: string) => void; clearNotification: () => void; }; }; export const NotificationContext = createContext<NotificationContextType | undefined>(undefined); export const NotificationProvider = ({ children }: { children: ReactNode }) => { const [status, setStatus] = useState<NotificationStatus | null>(null); const [title, setTitle] = useState<string>(""); const [message, setMessage] = useState<string | undefined>(""); const openNotification = (status: NotificationStatus, title: string, message?: string) => { setStatus(status); setTitle(title); setMessage(message); }; const clearNotification = () => { setStatus(null); setTitle(""); setMessage(""); }; return ( <NotificationContext.Provider value={{ status, title, message, actions: { openNotification, clearNotification, }, }} > {children} </NotificationContext.Provider> ); };
Create a file libs/hooks/useNotification.ts, then paste this code below
jsximport { useContext } from "react"; import { NotificationContext } from "@/libs/contexts/NotificationContext"; export const useNotification = () => { const context = useContext(NotificationContext); if (!context) { throw new Error("useNotification must be used within a NotificationProvider"); } return context; };
1.2 Wrap provider outside app
Now we need to wrap NotificationProvider outside the app. I often create separate file app/provider.tsx to handle all providers in app.
jsx"use client"; import React from "react"; import { NotificationProvider } from "@/libs/contexts/NotificationContext"; const Providers = ({ children }: { children: React.ReactNode }) => { return ( <NotificationProvider> {children} </NotificationProvider> ); }; export default Providers;
Then use Providers component in root app/layout.tsx
jsx<html lang="en"> <body> <Providers> {children} </Providers> </body> </html>
1.3 Create Notification component (UI)
Create a file components/ui/Notification.tsx, then paste this code below
jsx"use client"; import { useEffect } from "react"; import { Transition } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, ExclamationCircleIcon, XCircleIcon } from "@heroicons/react/24/outline"; import { useNotification } from "@/libs/hooks/useNotification"; type NotificationProps = { duration?: number; }; const Notification = ({ duration = 3000 }: NotificationProps) => { const { status, title, message, actions: { clearNotification }, } = useNotification(); useEffect(() => { const timer = setTimeout(() => { clearNotification(); }, duration); return () => clearTimeout(timer); }, [duration, clearNotification]); const renderIcon = () => { switch (status) { case "success": return <CheckCircleIcon aria-hidden="true" className="size-6 text-system-success" />; case "warning": return <ExclamationCircleIcon aria-hidden="true" className="size-6 text-system-waring" />; case "error": return <XCircleIcon aria-hidden="true" className="size-6 text-system-error" />; default: break; } }; return ( <div aria-live="assertive" className="pointer-events-none fixed inset-0 z-[900] flex items-end px-4 py-6 sm:items-start sm:p-6" > <div className="flex w-full flex-col items-center space-y-4 sm:items-end"> <Transition show={!!title || !!message}> <div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition data-[closed]:data-[enter]:translate-y-2 data-[enter]:transform data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-100 data-[enter]:ease-out data-[leave]:ease-in data-[closed]:data-[enter]:sm:translate-x-2 data-[closed]:data-[enter]:sm:translate-y-0"> <div className="p-4"> <div className="flex items-start"> <div className="flex-shrink-0">{renderIcon()}</div> <div className="ml-3 w-0 flex-1 pt-0.5"> <p className="text-sm font-medium text-gray-900">{title}</p> <p className="mt-1 text-sm text-gray-500">{message}</p> </div> <div className="ml-4 flex flex-shrink-0"> <button type="button" onClick={() => { clearNotification(); }} className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" > <span className="sr-only">Close</span> <XMarkIcon aria-hidden="true" className="h-5 w-5" /> </button> </div> </div> </div> </div> </Transition> </div> </div> ); }; export default Notification;
After creating component, we need to add this component in root of app, because we want to use it anywhere in our app.
jsximport Notification from "@/components/ui/Notification"; ... <html lang="en"> <body> <Providers> <Notification /> {children} </Providers> </body> </html>
All setups is done, now we can use it!
2. Usages
Now anywhere you can open notification for user, you can use Notification component. For example, I want to show the success or error message, when user click on the sign in button, I can do it like that
First, add hook
jsxconst { actions: { openNotification }, } = useNotification();
Add openNotification function in submit handle function (after call API, if get error we show notification with error message, and otherwise do it opposite).
jsxconst handleSubmit = async ( values: SigninFormValues, ) => { setIsLoading(true); try { const res = await signIn(SIGN_IN_PROVIDERS.CREDENTIALS, { email: values.email, password: values.password, callbackUrl, redirect: false, // Prevents full-page reload }); if (!res || res.error) { openNotification("error", AUTH_MESSAGE.EMAIL_OR_PASSWORD_INVALID); } else { window.location.href = callbackUrl; // Manually navigate after successful sign-in } } catch (error) { console.error("verify password fail***", error); openNotification("error", SYSTEM_MESSAGE.GENERAL_ERROR); } finally { setIsLoading(false); } };