September 08, 2024, (about 1 month ago) 1K page views

Extend Sonner for more expressive App notifications with React, Typescript, and Tailwind

Create an accessible notification system for your app built on top of Sonner.

sonner
notifications
accessibility
react
typescript
darkmode
tailwind

In this article, we will create a custom notification system for your app using React, Typescript, and Sonner - a library for creating accessible notifications.

Default sonner notifications are great, but they are not customizable. We will create a custom notification system that allows you to customize the notifications to fit your app's design.

Default Setup

First, let's install Sonner.

npm install sonner

Sonner provides a Toaster component that you can use to display notifications. You can use the toast method to create a notification.

import { Toaster, toast } from "sonner";

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button onClick={() => toast("My first toast")}>Give me a toast</button>
    </div>
  );
}

Custom Setup

We'll use the toast.custom() method to create a custom notification. This method takes a function that returns a React element. This allows us to create a custom notification with any design we want.

import { Toaster, toast } from "sonner";
import { Card } from "@/components/ui/card";

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button
        onClick={() =>
          toast.custom(() => (
            <Card className="w-full p-4 flex flex-col gap-2">
              My custom toast
            </Card>
          ))
        }
      >
        Give me a custom toast
      </button>
    </div>
  );
}

Easy, but we can do better. Let's wrap the toast api in something like notification, which let's us take full control of the design.

components/ui/notification.tsx

import { toast as toastRoot, ToasterProps } from "sonner";
import { Card } from "@/components/ui/card";
import { XIcon } from "lucide-react";

// Extend the ToasterProps interface,
// to include a title and description
interface Props extends ToasterProps {
  title: string;
  description: string;
}

const notification = {
  default: ({ title, description, ...props }: Props) => {
    toastRoot.custom(
      (t) => (
        <Card className="w-full p-4 flex flex-col gap-2 border-green-600 relative">
          <h2 className="text-sm">{props.title}</h2>
          <p className="text-xs">{description}</p>

          <Button
            variant="ghost"
            size={"icon-sm"}
            onClick={() => t.dismiss()}
            className="absolute top-2 right-2"
          >
            <XIcon size="12" />
          </Button>
        </Card>
      ),
      {
        // all options available can be passed here
        // https://sonner.emilkowal.ski/toast#api-reference
        ...props,
      }
    );
  },
  // success: ({ title, description, ...props }: Props) => {
  // ...
};

Now we can use our custom notification system like this:

import { notification } from "@/components/ui/notification";

function App() {
  return (
    <div>
      <Sonner />
      <button
        onClick={() =>
          notification.default({
            title: "My custom toast",
            description: "This is a custom toast",
          })
        }
      >
        Give me a custom toast
      </button>
    </div>
  );
}

Extending this notification system is easy. You can add more variants like success, warning, error, etc. You can also add more options to the Props interface to customize the notifications further.

Theming

To add themeing support, you can use the useTheme hook from your favorite theme provider. This will allow you to change the design of the notifications based on the current theme.

// file name: components/ui/notification.tsx
import { Toaster as Sonner, toast as toastRoot, ToasterProps } from "sonner";
import { Card } from "@/components/ui/card";
import { useTheme } from "next-themes";

type ToasterProps = React.ComponentProps<typeof Sonner>;

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme();
  return <Sonner theme={theme as ToasterProps["theme"]} {...props} />;
};

//...

And so instead of importing Sonner directly, you can import the Toaster component from your custom notification component.

import { Toaster } from "@/components/ui/notification";

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button
        onClick={() =>
          notification.default({
            title: "My custom toast",
            description: "This is a custom toast",
          })
        }
      >
        Give me a custom toast
      </button>
    </div>
  );
}

Conclusion

Creating a custom notification system with Sonner is easy. You can customize the notifications to fit your app's design and add theming support to change the design based on the current theme.