How to create React Toasts/Notifications with 0 dependencies
Tags: javascript, react, css
Published: 2020-11-03 23:23:51

---
title: How to create React Toasts/Notifications with 0 dependencies
tags: ["javascript", "react", "css"]
cover_image: https://res.cloudinary.com/https-tinloof-com/image/upload/v1602609796/blog/how-to-create-react-notifications-with-0-dependencies/6_d3s7q4.gif
canonical_url: https://tinloof.com/blog/how-to-create-react-notifications-with-0-dependencies/
published: true
cover_image: ""
---

> Article originally published on [Tinloof](https://tinloof.com/blog/how-to-create-react-notifications-with-0-dependencies/).

In this article, we'll build from scratch React Notifications (toasts)  without using any third-party library (except React).

![Final result](https://dev-to-uploads.s3.amazonaws.com/i/2jbgyc0n9w7e7tl8sx67.gif)

The notification component has the following requirements:

1. Four color variations: info (blue), success (green), warning (orange), and error (red).
2. It's positioned on the top right of the screen.
3. It's animated to slide in when it's added and slide out when it's removed. The other notifications should slide vertically when a notification is removed.
4. I can create notifications that close automatically after 10 seconds.
5. I can create notifications declaratively in JSX (e.g `<Notification color="success" />`).
6. I can create notifications imperatively by calling a function (e.g. `success()`).

The final source code can be found [here](https://github.com/tinloof/notify) and a demo can be viewed [here](https://codesandbox.io/s/tinloof-notify-hd8pt?file=/src/App.js).

# Note about the boilerplate and CSS in this article

I used [create-react-app](https://reactjs.org/docs/create-a-new-react-app.html) to generate the boilerplate for this project and [CSS modules](https://github.com/css-modules/css-modules) to style it.

You're free to use any other tools to generate the boilerplate and style the component.

# Creating the Notification component

Here's our directory structure, we'll go through every single file in it:

```
├── App.css
├── App.js
├── index.css
├── index.js
└── notify
   ├── Notification
   |  ├── Notification.module.css
   |  ├── index.js
   |  └── times.svg
   ├── createContainer
   |  ├── container.module.css
   |  └── index.js
   └── index.js
```

## The Notification component

```js
// notify/Notification/index.js

import React from "react";
import PropTypes from "prop-types";
import cn from "classnames";

import { ReactComponent as Times } from "./times.svg";
import styles from "./Notification.module.css";

export default function Notification({ color = Color.info, children }) {
  return (
    <div className={cn([styles.notification, styles[color]])}>
      {children}
      <button className={styles.closeButton}>
        <Times height={16} />
      </button>
    </div>
  );
}

export const Color = {
  info: "info",
  success: "success",
  warning: "warning",
  error: "error",
};

Notification.propTypes = {
  notificationType: PropTypes.oneOf(Object.keys(Color)),
  children: PropTypes.element,
};
```

The `Notification` component so far has 2 props:

- `color`: a string value that determines the background color of the notification and can be either info, success, warning, or error.
- `children`: any React elements we want to render inside the notification.

And here are its styles:

```css
/* notify/Notification/Notification.module.css */

.notification {
  max-width: 430px;
  max-height: 200px;
  overflow: hidden;
  padding: 12px 48px 12px 12px;
  z-index: 99;
  font-weight: bold;
  position: relative;
}

.notification:not(:last-child) {
  margin-bottom: 8px;
}

.notification.info {
  background-color: #2196f3;
}

.notification.success {
  background-color: #4caf50;
}

.notification.warning {
  background-color: #ff9800;
}

.notification.error {
  background-color: #f44336;
}

.notification .closeButton {
  position: absolute;
  top: 12px;
  right: 12px;
  background: transparent;
  padding: 0;
  border: none;
  cursor: pointer;
}

.notification, .notification .closeButton {
  color: #fff;
}
```

# Rendering notifications in the document's body

Our notifications should be rendered separately from the DOM structure of the application using them. 

 `createContainer` is a helper function that creates a container element for the notifications (if it doesn't exist already) and append it directly to the document's body:

```js
// notify/createContainer/index.js
import styles from "./container.module.css";

export default function createContainer() {
  const portalId = "notifyContainer";
  let element = document.getElementById(portalId);

  if (element) {
    return element;
  }

  element = document.createElement("div");
  element.setAttribute("id", portalId);
  element.className = styles.container;
  document.body.appendChild(element);
  return element;
}
```

It has a `fixed` position and is placed on the top right as per our requirements:

```css
/* notify/createContainer/container.module.css */

.container {
  position: fixed;
  top: 16px;
  right: 16px;
}
```

We can then use `ReactDOM.createPortal` to render the notification in the container we create:

```js
// notify/Notification/index.js

const container = createContainer();

export default function Notification({ color = Color.info, children }) {
  return createPortal(
    <div className={cn([styles.notification, styles[color]])}>
      {children}
      <button className={styles.closeButton}>
        <Times height={16} />
      </button>
    </div>,
    container
  );
}
```

## First demo

Before writing a demo, let's expose `Notification` and its `Color` object in `notify/index.js` so that they can be imported and used:

```js
// notify/index.js

export { default as Notification, Color } from "./Notification";
```

Now let's write a demo to showcase the different notifications:

```js
// App.js

import React from "react";
import "./App.css";
import { Notification, Color } from "./notify";

function App() {
  const [notifications, setNotifications] = React.useState([]);

  const createNotification = (color) =>
    setNotifications([...notifications, { color, id: notifications.length }]);

  return (
    <div className="App">
      <h1>Notification Demo</h1>
      <button onClick={() => createNotification(Color.info)}>Info</button>
      <button onClick={() => createNotification(Color.success)}>Success</button>
      <button onClick={() => createNotification(Color.warning)}>Warning</button>
      <button onClick={() => createNotification(Color.error)}>Error</button>
      {notifications.map(({ id, color }) => (
        <Notification key={id} color={color}>
          This is a notification!
        </Notification>
      ))}
    </div>
  );
}

export default App;
```

Our demo simply renders a list of notifications and has 4 different buttons to add colored notifications to our list.

<figure>
  <img loading="lazy"
    src="https://res.cloudinary.com/https-tinloof-com/image/upload/v1602092066/blog/how-to-create-react-notifications-with-0-dependencies/1_vdnumo.png"
  />
  <figcaption>First demo: showing basic colored notifications</figcaption>
</figure>


# Closing notifications

Let's make it possible to close notifications by adding an `onDelete` prop to `Notification` and making the close button invoke that function on click:

```js
// notify/Notification/index.js

export default function Notification({
  color = Color.info,
  onDelete,
  children,
}) {
  return createPortal(
    <div className={cn([styles.notification, styles[color]])}>
      {children}
      <button onClick={onDelete} className={styles.closeButton}>
        <Times height={16} />
      </button>
    </div>,
    container
  );
}
```

Now, in `App.js`, we pass an `onDelete` prop function that deletes the corresponding notification from the list:

```js
// App.js

function App() {
  const [notifications, setNotifications] = React.useState([]);

  const createNotification = (color) =>
    setNotifications([...notifications, { color, id: notifications.length }]);

  const deleteNotification = (id) =>
    setNotifications(
      notifications.filter((notification) => notification.id !== id)
    );

  return (
    <div className="App">
      <h1>Notification Demo</h1>
      <button onClick={() => createNotification(Color.info)}>Info</button>
      <button onClick={() => createNotification(Color.success)}>Success</button>
      <button onClick={() => createNotification(Color.warning)}>Warning</button>
      <button onClick={() => createNotification(Color.error)}>Error</button>
      {notifications.map(({ id, color }) => (
        <Notification
          key={id}
          onDelete={() => deleteNotification(id)}
          color={color}
        >
          This is a notification!
        </Notification>
      ))}
    </div>
  );
}
```

<figure>
  <img loading="lazy"
    src="https://res.cloudinary.com/https-tinloof-com/image/upload/v1602092588/blog/how-to-create-react-notifications-with-0-dependencies/2_yejaly.gif"
  />
  <figcaption>Closing notifications</figcaption>
</figure>


# Adding "slide-in" and "slide-out" animations

Notifications are added and deleted too fast, which might confuse users. By adding "slide-in" and "slide-out" animations, we make notifications behave more naturally and improve the user experience.

To slide the notification in, we simply use the `translateX` CSS transform and translate it from 100% to 0. Here's the corresponding animation created with [keyframes](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes):

```css
/* notify/Notification/Notification.module.css */

@keyframes slideIn {
  from {
    transform: translateX(100%);
  }

  to {
    transform: translateX(0%);
  }
}

.notification.slideIn {
  animation-name: slideIn;
  animation-duration: 0.3s;
  animation-timing-function: ease-in-out;
}
```

"slide-out" is a bit more tricky. When hitting the close button, we need to have a "closing" phase before calling the `onDelete` prop function. During the closing phase, we can slide the notification out using `translateX(150%)` and add a transition to `notification` to smoothen the "slide-out".

Here are the styles corresponding to the "slide-out" animation:

```css
/* notify/Notification/Notification.module.css */

.notification {
  ...
  transition: transform 0.3s ease-out;
}

.notification.slideOut {
  transform: translateX(150%);
  flex: 0;
}
```

To achieve the closing phase in `Notification`, we can use a boolean state variable `isClosing` (set to `false` by default) . When we hit the close button, we set `isClosing` to `true` , wait for a the transition duration (`300ms` here), and then call the `onDelete` function.

We only use the `slideIn` animation styles when we're not in the closing phase (i.e. `isClosing=false`) and `slideOut` animation styles when we're in the closing phase (i.e. `isCloseing=true`).

```js
// notify/Notification/index.js

let timeToDelete = 300;

export default function Notification({
  color = Color.info,
  onDelete,
  children,
}) {
  const [isClosing, setIsClosing] = React.useState(false);

  React.useEffect(() => {
    if (isClosing) {
      const timeoutId = setTimeout(onDelete, timeToDelete);

      return () => {
        clearTimeout(timeoutId);
      };
    }
  }, [isClosing, onDelete]);

  return createPortal(
    <div
      className={cn([
        styles.notification,
        styles[color],
        { [styles.slideIn]: !isClosing },
        { [styles.slideOut]: isClosing },
      ])}
    >
      {children}
      <button onClick={() => setIsClosing(true)} className={styles.closeButton}>
        <Times height={16} />
      </button>
    </div>,
    container
  );
}
```

<figure>
  <img loading="lazy"
    src="https://res.cloudinary.com/https-tinloof-com/image/upload/v1602094080/blog/how-to-create-react-notifications-with-0-dependencies/3_qrb1en.gif"
  />
  <figcaption>Adding "slide-in" and "slide-out" animations</figcaption>
</figure>

# Animating notifications shift

When a notification is deleted, the ones below it shift suddenly to the top to fill up its position.

To make this shift more natural, let's add a container around the notification that shrinks smoothly during the closing phase:

```js
// notify/Notification/index.js

let timeToDelete = 300;

export default function Notification({
  color = Color.info,
  onDelete,
  children,
}) {
  const [isClosing, setIsClosing] = React.useState(false);

  React.useEffect(() => {
    if (isClosing) {
      const timeoutId = setTimeout(onDelete, timeToDelete);

      return () => {
        clearTimeout(timeoutId);
      };
    }
  }, [isClosing, onDelete]);

  return createPortal(
    <div className={cn([styles.container, { [styles.shrink]: isClosing }])}>
      <div
        className={cn([
          styles.notification,
          styles[color],
          { [styles.slideIn]: !isClosing },
          { [styles.slideOut]: isClosing },
        ])}
      >
        {children}
        <button
          onClick={() => setIsClosing(true)}
          className={styles.closeButton}
        >
          <Times height={16} />
        </button>
      </div>
    </div>,
    container
  )
```

The container has a `max-height` of `200px` by default and shrinks to `0` during the closing phase. We should also move the `margin` definition to the container:

```css
/* notify/Notification/Notification.module.css */

.container {
  overflow: hidden;
  max-height: 200px;
  transition: max-height 0.3s ease-out;
}

.container:not(:last-child) {
  margin-bottom: 8px;
}

.container.shrink {
  max-height: 0;
}
```

<figure>
  <img loading="lazy"
    src="https://res.cloudinary.com/https-tinloof-com/image/upload/v1602094569/blog/how-to-create-react-notifications-with-0-dependencies/4_zqfc6z.gif"
  />
  <figcaption>Animating notifications shift</figcaption>
</figure>

# Making notifications close automatically

Let's add an `autoClose` boolean prop to the Notification component and use `useEffect` to close the notification after 10 seconds if the prop is set to true.

```js
// notify/Notification/index.js

export default function Notification({
  color = Color.info,
  autoClose = false,
  onDelete,
  children,
}) {
  const [isClosing, setIsClosing] = React.useState(false);

  React.useEffect(() => {
    if (autoClose) {
      const timeoutId = setTimeout(() => setIsClosing(true), timeToClose);

      return () => {
        clearTimeout(timeoutId);
      };
    }
  }, [autoClose]);
```

Now let's modify our demo to pass `autoClose=true` to the notifications:

```js
// App.js

function App() {
  const [notifications, setNotifications] = React.useState([]);

  const createNotification = (color) =>
    setNotifications([...notifications, { color, id: notifications.length }]);

  const deleteNotification = (id) =>
    setNotifications(
      notifications.filter((notification) => notification.id !== id)
    );

  return (
    <div className="App">
      <h1>Notification Demo</h1>
      <button onClick={() => createNotification(Color.info)}>Info</button>
      <button onClick={() => createNotification(Color.success)}>Success</button>
      <button onClick={() => createNotification(Color.warning)}>Warning</button>
      <button onClick={() => createNotification(Color.error)}>Error</button>
      {notifications.map(({ id, color }) => (
        <Notification
          key={id}
          onDelete={() => deleteNotification(id)}
          color={color}
          autoClose={true}
        >
          This is a notification!
        </Notification>
      ))}
    </div>
  );
}
```

Now notifications close automatically after 10 seconds of their creation:

<figure>
  <img loading="lazy"
    src="https://res.cloudinary.com/https-tinloof-com/image/upload/v1602606400/blog/how-to-create-react-notifications-with-0-dependencies/5_ekuzqv.gif"
  />
  <figcaption>Making notifications close automatically</figcaption>
</figure>

# Creating notifications imperatively

We want to be able to create notifications imperatively, by calling functions such as `success()` or `error()`.

The trick is to create a component similar to our `App` one that is rendered by default and provides us a function to create notifications.

Let's create `NotificationsManager` to serve that purpose:

```js
// notify/NotificationsManager

import React from "react";
import PropTypes from "prop-types";

import Notification from "./Notification";

export default function NotificationsManager({ setNotify }) {
  let [notifications, setNotifications] = React.useState([]);

  let createNotification = ({ color, autoClose, children }) => {
    setNotifications((prevNotifications) => {
      return [
        ...prevNotifications,
        {
          children,
          color,
          autoClose,
          id: prevNotifications.length,
        },
      ];
    });
  };

  React.useEffect(() => {
    setNotify(({ color, autoClose, children }) =>
      createNotification({ color, autoClose, children })
    );
  }, [setNotify]);

  let deleteNotification = (id) => {
    const filteredNotifications = notifications.filter(
      (_, index) => id !== index,
      []
    );
    setNotifications(filteredNotifications);
  };

  return notifications.map(({ id, ...props }, index) => (
    <Notification
      key={id}
      onDelete={() => deleteNotification(index)}
      {...props}
    />
  ));
}

NotificationsManager.propTypes = {
  setNotify: PropTypes.func.isRequired,
};
```

NotificationsManager receives one prop `setNotify` , which is used to give access to the `createNotification` function to create notifications imperatively.

Now let's render `NotificationsManager` in the same container as Notfication and create our notification creation functions. We access `createNotification` function through the `setNotify` prop and use it to create our notification creation functions:

```js
// notify/index.js

import React from "react";
import ReactDOM from "react-dom";

import NotificationsManager from "./NotificationsManager";
import Notification, { Color } from "./Notification";
import createContainer from "./createContainer";

const containerElement = createContainer();
let notify;

ReactDOM.render(
  <NotificationsManager
    setNotify={(notifyFn) => {
      notify = notifyFn;
    }}
  />,
  containerElement
);

export { Notification, Color };

export function info(children, autoClose) {
  return notify({
    color: Color.info,
    children,
    autoClose,
  });
}

export function success(children, autoClose) {
  return notify({
    color: Color.success,
    children,
    autoClose,
  });
}

export function warning(children, autoClose) {
  return notify({
    color: Color.warning,
    children,
    autoClose,
  });
}

export function error(children, autoClose) {
  return notify({
    color: Color.error,
    children,
    autoClose,
  });
}
```

Now let's test these functions out in `App.js` . Let's also make 2 changes to improve our demo:

- Make it possible to show both declarative and imperative approaches.
- Use [react-highlight](https://www.npmjs.com/package/react-highlight) to show a code snippet for each approach.

```js
// App.js

import React from "react";
import Highlight from "react-highlight";

import "./App.css";
import "./highlight-js-night-owl.css";

import { Notification, Color, info, success, warning, error } from "./notify";

const message = "This is a notification!";

function DeclarativeDemo() {
  const [notifications, setNotifications] = React.useState([]);

  const createNotification = (color) =>
    setNotifications([...notifications, { color, id: notifications.length }]);

  const deleteNotification = (id) =>
    setNotifications(
      notifications.filter((notification) => notification.id !== id)
    );

  return (
    <>
      <Highlight>
        {`const [notifications, setNotifications] = React.useState([]);

const createNotification = (color) =>
  setNotifications([...notifications, { color, id: notifications.length }]);

const deleteNotification = (id) =>
  setNotifications(
    notifications.filter((notification) => notification.id !== id)
  );

return (
  <>
    <button onClick={() => createNotification(Color.info)}>Info</button>
    <button onClick={() => createNotification(Color.success)}>Success</button>
    <button onClick={() => createNotification(Color.warning)}>Warning</button>
    <button onClick={() => createNotification(Color.error)}>Error</button>
    {notifications.map(({ id, color }) => (
      <Notification
        key={id}
        onDelete={() => deleteNotification(id)}
        color={color}
        autoClose={true}
      >
        {message}
      </Notification>
    ))}
  </>
);`}
      </Highlight>
      <button onClick={() => createNotification(Color.info)}>Info</button>
      <button onClick={() => createNotification(Color.success)}>Success</button>
      <button onClick={() => createNotification(Color.warning)}>Warning</button>
      <button onClick={() => createNotification(Color.error)}>Error</button>
      {notifications.map(({ id, color }) => (
        <Notification
          key={id}
          onDelete={() => deleteNotification(id)}
          color={color}
          autoClose={true}
        >
          {message}
        </Notification>
      ))}
    </>
  );
}

function ImperativeDemo() {
  return (
    <>
      <Highlight>
        {`<>
  <button onClick={() => info(message, true)}>Info</button>
  <button onClick={() => success(message, true)}>Success</button>
  <button onClick={() => warning(message, true)}>Warning</button>
  <button onClick={() => error(message, true)}>Error</button>
</>`}
      </Highlight>
      <button onClick={() => info(message, true)}>Info</button>
      <button onClick={() => success(message, true)}>Success</button>
      <button onClick={() => warning(message, true)}>Warning</button>
      <button onClick={() => error(message, true)}>Error</button>
    </>
  );
}

function App() {
  const [demo, setDemo] = React.useState("declarative");

  return (
    <div className="App">
      <select onChange={(e) => setDemo(e.target.value)}>
        <option value="declarative">Declarative demo</option>
        <option value="imperative">Imperative demo</option>
      </select>
      {demo === "declarative" ? <DeclarativeDemo /> : <ImperativeDemo />}
    </div>
  );
}

export default App;
```

![Final result](https://dev-to-uploads.s3.amazonaws.com/i/2jbgyc0n9w7e7tl8sx67.gif)

> **Note:** we'd love to help you build beautiful and fast web applications. [Get in touch with us](https://tinloof.com/contact).