Gustavo Willemann

Customize the selected theme colors

NameColor / Text
Background
Box
Primary
Secundary
Success
Error

An Event Toast

This article is a step-by-step on building a React Toast Component using the CustomEvent Web API. A demonstration of the component is available in a Codepen project.

Toasting glasses

The project starts with the following App component:

function App() {
return (
<div>
<button></button>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);

First, let's create the ToastContainer component. For now, it's only a div with a static text and the class 'toast-container'.

export default function ToastContainer() {
return (
<div className="toast-container">
Hello toast!
</div>
);
}

Next, add the container to the App component. I choose to place it as a sibling to the 'app-container' div. Here the 'React Fragments' are being used so that both the 'toast-container' and the 'app-container' will be children of the '#root' div.

<>
<div className="app-container">
<button>Toast</button>
<div>
<ToastContainer />
</>

Now, let's add some basic styling.

Here we are positioning the ToastContainer on the top left corner of the screen, with the position, top, and left properties. The component will also receive padding, background color, and a border that has its color defined by the type of Toast message.

.toast-container {
position: fixed;
top: 0px;
left: 10px;
padding: 20px;
border: 4px solid transparent;
background-color: #222;
}
.toast-container.success {
border-color: green;
}
.toast-container.error {
border-color: red;
}

Using the React useState hook, we'll create the constant that will hold the Toast data. The state will be an object with the attributes message and type, and it'll be nullable.

With the Toast type, we can create the class name used to change the border color. The class name is an empty string if the Toast data is null otherwise, it will be a string with a blank space followed by the type name.

The Toast type class name is added to the 'toast-container' class name.

export default function ToastContainer() {
const [toastData, setToastData] = useState(null);
const typeClassName = toastData ? ' ' + toastData.type : '';
return (
<div
className={'toast-container' + typeClassName}
>
{toastData?.message}
</div>
);
}

Now we code the function that will show the Toast message, creating the trigger event with the CustomEvent API. It also dispatches the event into the document.

The CustomEvent API receives two arguments the first is the event name the second is an options object. It has the details attribute, which we'll use to carry the Toast data ( object with attributes message and type ).

A constant holds the event name, which will also be used when adding the event listener.

const toastTriggerEvent = 'toastTrigger';
function showToast(message, type = 'default') {
const toastEvent = new CustomEvent(toastTriggerEvent, { detail: { message, type } });
document.dispatchEvent(toastEvent);
}

In the ToastComponent, we add the event listener. In React, event listeners are added using the useEffect hook. The event callback is a function that will update the toastData state with the data from the event detail field.

The useEffect hook needs a cleanup function to remove the event listener.

export default function ToastContainer() {
const [toastData, setToastData] = useState(null);
React.useEffect(() => {
const eventCallback = ({ detail }) => setToastData(detail);
document.addEventListener(toastTriggerEvent, eventCallback);
return () => document.removeEventListener(toastTriggerEvent, eventCallback); // cleanup function
}, []);
return (
<div className="toast-container">
{toastData?.message}
</div>
);
}

The toast message can be removed using a timeout, also done using the useEffect hook. In this case, the timeout callback will set the toastData state to null.

Like the previous useEffect, this one also needs a cleanup function to clear the timeout.

useEffect(() => {
if (!toastData) return;
const callback = () => setToastData(null);
const timeoutId = setTimeout(callback, 1500);
return () => clearTimeout(timeoutId); // cleanup function
}, [toastData]);

Now we can render the ToastContainer only when the toast data is available.

// ToastConainer Component
if (toastData) return (
<div
className={'toast-container' + typeClassName}
>
{toastData.message}
</div>
)
return null;
}

Adding an animation

The ToastComponent is already functional, but we can make it look prettier with an animation. We'll handle it with the CSS @keyframes. In this case the animation is an slide donw and slide up.

@keyframes showToast {
0% {
transform: translateY(-100%);
}
10% {
transform: translateY(10px);
}
90% {
transform: translateY(10px);
}
100% {
transform: translateY(-100%);
}
}

Add the @keyframes in the component with the animation property.

.toast-container {
animation: showToast 2000ms forwards ease-in-out;
/* other props */
}

We also need to add the Key prop to the ToastContainer so ReactJS will create a new element each time a Toast is triggered instead of updating the existing one, which already played the animation.

In this case, the value from Date.now() will suffice.

<div
key={Date.now()}
className={'toast-container' + typeClassName}
>
{toastData.message}
</div>

If we want to keep using a timeout to clear the toast, we need to update the timeout to match the animation duration. But a better way to do it is using the 'animationend' event.

For that, we need to access the toast-container div with the useRef hook. With the ref, we can add an event listener to the element. It will replace the timeout useEffect.

export default function ToastContainer() {
const [toastData, setToastData] = React.useState(null);
const toastContainer = React.useRef();
React.useEffect(() => {
if (!toastData || !toastContainer.current) return;
const toastContainerEl = toastContainer.current;
const eventCallback = () => setToastData(null);
toastContainerEl.addEventListener('animationend', eventCallback);
return () => toastContainerEl.removeEventListener('animationend', eventCallback);
}, [toastData, toastContainer]);
// ToastContainer
}

Keeping the message on hover

The last feature will be to keep the message when hovered. Our implementation of the component makes it easy to add that feature. Since the message is removed based on the animationend event, we can pause the animation to prevent its removal using the CSS selector :hover.

.toast-container:hover {
animation-play-state: paused;
}