Are you looking to implement elegant and lightweight toast notifications into your web app? Look no further. With the JS library that I've developed, you can seamlessly output your message notifications with a few lines of code.

The library leverages CSS transitions to create a fluid experience and provides configurable options to tailor the notifications to your requirements. No jQuery, no dependency, just pure JS.

1. What are Toast Notifications?

Toast notifications often appear in the corner of a website that notifies the user with a message, which could represent an error, success, or custom message. They're basically a lightweight variant of browser notifications, except they will only appear on a particular webpage.

Imagine the following scenario — you have a login form but don't want the user to be redirected to a separate page with the response message because they've entered incorrect details. That's where toast notifications can come in handy. Instead of redirecting the user, you can output the validation response with toast notifications, seamlessly. And all the input data on that page will be preserved.

When we think toast we think white or brown? French or British? But no... In terms of programming, the toast keyword prevents confusion with the browser notifications because while they're similar, their purpose is much different.

2. Stylesheet

Below is the stylesheet (CSS3) for the toast notifications library. You can either create a separate CSS file or incorporate it into your existing CSS file.

CSS
.toast-notification {
  position: fixed;
  text-decoration: none;
  z-index: 999999;
  max-width: 300px;
  background-color: #fff;
  box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.12);
  border-radius: 4px;
  display: flex;
  padding: 10px;
  transform: translate(0, -150%);
}
.toast-notification .toast-notification-wrapper {
  flex: 1;
  padding-right: 10px;
  overflow: hidden;
}
.toast-notification .toast-notification-wrapper .toast-notification-header {
  padding: 0 0 5px 0;
  margin: 0;
  font-weight: 500;
  font-size: 14px;
  word-break: break-all;
  color: #4f525a;
}
.toast-notification .toast-notification-wrapper .toast-notification-content {
  font-size: 14px;
  margin: 0;
  padding: 0;
  word-break: break-all;
  color: #4f525a;
}
.toast-notification .toast-notification-close {
  appearance: none;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 24px;
  line-height: 24px;
  padding-bottom: 4px;
  font-weight: bold;
  color: rgba(0, 0, 0, 0.2);
}
.toast-notification .toast-notification-close:hover {
  color: rgba(0, 0, 0, 0.4);
}
.toast-notification.toast-notification-top-center {
  transform: translate(calc(50vw - 50%), -150%);
}
.toast-notification.toast-notification-bottom-left, .toast-notification.toast-notification-bottom-right {
  transform: translate(0, 150%);
}
.toast-notification.toast-notification-bottom-center {
  transform: translate(calc(50vw - 50%), 150%);
}
.toast-notification.toast-notification-dark {
  background-color: #2d2e31;
}
.toast-notification.toast-notification-dark .toast-notification-wrapper .toast-notification-header {
  color: #edeff3;
}
.toast-notification.toast-notification-dark .toast-notification-wrapper .toast-notification-content {
  color: #edeff3;
}
.toast-notification.toast-notification-dark .toast-notification-close {
  color: rgba(255, 255, 255, 0.2);
}
.toast-notification.toast-notification-dark .toast-notification-close:hover {
  color: rgba(255, 255, 255, 0.4);
}
.toast-notification.toast-notification-success {
  background-color: #C3F3D7;
  border-left: 4px solid #51a775;
}
.toast-notification.toast-notification-success .toast-notification-wrapper .toast-notification-header {
  color: #51a775;
}
.toast-notification.toast-notification-success .toast-notification-wrapper .toast-notification-content {
  color: #51a775;
}
.toast-notification.toast-notification-success .toast-notification-close {
  color: rgba(0, 0, 0, 0.2);
}
.toast-notification.toast-notification-success .toast-notification-close:hover {
  color: rgba(0, 0, 0, 0.4);
}
.toast-notification.toast-notification-error {
  background-color: #f3c3c3;
  border-left: 4px solid #a75151;
}
.toast-notification.toast-notification-error .toast-notification-wrapper .toast-notification-header {
  color: #a75151;
}
.toast-notification.toast-notification-error .toast-notification-wrapper .toast-notification-content {
  color: #a75151;
}
.toast-notification.toast-notification-error .toast-notification-close {
  color: rgba(0, 0, 0, 0.2);
}
.toast-notification.toast-notification-error .toast-notification-close:hover {
  color: rgba(0, 0, 0, 0.4);
}
.toast-notification.toast-notification-verified {
  background-color: #d0eaff;
  border-left: 4px solid #6097b8;
}
.toast-notification.toast-notification-verified .toast-notification-wrapper .toast-notification-header {
  color: #6097b8;
}
.toast-notification.toast-notification-verified .toast-notification-wrapper .toast-notification-content {
  color: #6097b8;
}
.toast-notification.toast-notification-verified .toast-notification-close {
  color: rgba(0, 0, 0, 0.2);
}
.toast-notification.toast-notification-verified .toast-notification-close:hover {
  color: rgba(0, 0, 0, 0.4);
}
.toast-notification.toast-notification-dimmed {
  opacity: .3;
}
.toast-notification.toast-notification-dimmed:hover, .toast-notification.toast-notification-dimmed:active {
  opacity: 1;
}

I've prefixed the class names to prevent them from conflicting with other libraries.

3. Toast Notifications JS Library

Create the Toasts.js file and add the following code:

JS
"use strict";
class Toasts {

    constructor(options) {
        let defaults = {
            position: 'top-right',
            stack: [],
            offsetX: 20,
            offsetY: 20,
            gap: 20,
            numToasts: 0,
            duration: '.5s',
            timing: 'ease',
            dimOld: true
        };
        this.options = Object.assign(defaults, options);
    }

    push(obj) {
        this.numToasts++;
        let toast = document.createElement(obj.link ? 'a' : 'div');
        if (obj.link) {
            toast.href = obj.link;
            toast.target = obj.linkTarget ? obj.linkTarget : '_self';
        }
        toast.className = 'toast-notification' + (obj.style ? ' toast-notification-' + obj.style : '') + ' toast-notification-' + this.position;
        toast.innerHTML = `
            <div class="toast-notification-wrapper">
                ${obj.title ? '<h3 class="toast-notification-header">' + obj.title + '</h3>' : ''}
                ${obj.content ? '<div class="toast-notification-content">' + obj.content + '</div>' : ''}
            </div>
            ${obj.closeButton == null || obj.closeButton === true ? '<button class="toast-notification-close">&times;</button>' : ''}
        `;
        document.body.appendChild(toast);
        toast.getBoundingClientRect();
        if (this.position == 'top-left') {
            toast.style.top = 0;
            toast.style.left = this.offsetX + 'px';
        } else if (this.position == 'top-center') {
            toast.style.top = 0;
            toast.style.left = 0;
        } else if (this.position == 'top-right') {
            toast.style.top = 0;
            toast.style.right = this.offsetX + 'px';
        } else if (this.position == 'bottom-left') {
            toast.style.bottom = 0;
            toast.style.left = this.offsetX + 'px';
        } else if (this.position == 'bottom-center') {
            toast.style.bottom = 0;
            toast.style.left = 0;
        } else if (this.position == 'bottom-right') {
            toast.style.bottom = 0;
            toast.style.right = this.offsetX + 'px';
        }
        if (obj.width || this.width) {
            toast.style.width = (obj.width || this.width) + 'px';
        }
        toast.dataset.transitionState = 'queue';
        let index = this.stack.push({ element: toast, props: obj, offsetX: this.offsetX, offsetY: this.offsetY, index: 0 });
        this.stack[index-1].index = index-1;
        if (toast.querySelector('.toast-notification-close')) {
            toast.querySelector('.toast-notification-close').onclick = event => {
                event.preventDefault();
                this.closeToast(this.stack[index-1]);
            };
        }
        if (obj.link) {
            toast.onclick = () => this.closeToast(this.stack[index-1]);
        }
        this.openToast(this.stack[index-1]);
        if (obj.onOpen) obj.onOpen(this.stack[index-1]);
    }

    openToast(toast) {
        if (this.isOpening() === true) {
            return false;
        }
        toast.element.dataset.transitionState = 'opening';
        toast.element.style.transition = this.duration + ' transform ' + this.timing;
        this._transformToast(toast);
        toast.element.addEventListener('transitionend', () => {
            if (toast.element.dataset.transitionState == 'opening') {
                toast.element.dataset.transitionState = 'complete';
                for (let i = 0; i < this.stack.length; i++) {
                    if (this.stack[i].element.dataset.transitionState == 'queue') {
                        this.openToast(this.stack[i]);
                    }
                }
                if (toast.props.dismissAfter) {
                    this.closeToast(toast, toast.props.dismissAfter);
                }
            }
        });
        for (let i = 0; i < this.stack.length; i++) {
            if (this.stack[i].element.dataset.transitionState == 'complete') {
                this.stack[i].element.dataset.transitionState = 'opening';
                this.stack[i].element.style.transition = this.duration + ' transform ' + this.timing + (this.dimOld ? ', ' + this.duration + ' opacity ease' : '');
                if (this.dimOld) {
                    this.stack[i].element.classList.add('toast-notification-dimmed');
                }
                this.stack[i].offsetY += toast.element.offsetHeight + this.gap;
                this._transformToast(this.stack[i]);
            }
        }
        return true;
    }

    closeToast(toast, delay = null) {
        if (this.isOpening() === true) {
            setTimeout(() => this.closeToast(toast, delay), 100);
            return false;
        }
        if (toast.element.dataset.transitionState == 'close') {
            return true;
        }
        if (toast.element.querySelector('.toast-notification-close')) {
            toast.element.querySelector('.toast-notification-close').onclick = null;
        }
        toast.element.dataset.transitionState = 'close';
        toast.element.style.transition = '.2s opacity ease' + (delay ? ' ' + delay : '');
        toast.element.style.opacity = 0;
        toast.element.addEventListener('transitionend', () => {
            if (toast.element.dataset.transitionState == 'close') {
                let offsetHeight = toast.element.offsetHeight;
                if (toast.props.onClose) toast.props.onClose(toast);
                toast.element.remove();
                for (let i = 0; i < toast.index; i++) {
                    this.stack[i].element.style.transition = this.duration + ' transform ' + this.timing;
                    this.stack[i].offsetY -= offsetHeight + this.gap;
                    this._transformToast(this.stack[i]);
                }
                let focusedToast = this.getFocusedToast();
                if (focusedToast) {
                    focusedToast.element.classList.remove('toast-notification-dimmed');
                }
            }
        });
        return true;
    }

    isOpening() {
        let opening = false;
        for (let i = 0; i < this.stack.length; i++) {
            if (this.stack[i].element.dataset.transitionState == 'opening') {
                opening = true;
            }
        }
        return opening;
    }

    getFocusedToast() {
        for (let i = 0; i < this.stack.length; i++) {
            if (this.stack[i].offsetY == this.offsetY) {
                return this.stack[i];
            }
        }
        return false;
    }

    _transformToast(toast) {
        if (this.position == 'top-center') {
            toast.element.style.transform = `translate(calc(50vw - 50%), ${toast.offsetY}px)`;
        } else if (this.position == 'top-right' || this.position == 'top-left') {
            toast.element.style.transform = `translate(0, ${toast.offsetY}px)`;
        } else if (this.position == 'bottom-center') {
            toast.element.style.transform = `translate(calc(50vw - 50%), -${toast.offsetY}px)`;            
        } else if (this.position == 'bottom-left' || this.position == 'bottom-right') {
            toast.element.style.transform = `translate(0, -${toast.offsetY}px)`;
        }
    }

    set stack(value) {
        this.options.stack = value;
    }

    get stack() {
        return this.options.stack;
    }

    set position(value) {
        this.options.position = value;
    }

    get position() {
        return this.options.position;
    }

    set offsetX(value) {
        this.options.offsetX = value;
    }

    get offsetX() {
        return this.options.offsetX;
    }

    set offsetY(value) {
        this.options.offsetY = value;
    }

    get offsetY() {
        return this.options.offsetY;
    }

    set gap(value) {
        this.options.gap = value;
    }

    get gap() {
        return this.options.gap;
    }

    set numToasts(value) {
        this.options.numToasts = value;
    }

    get numToasts() {
        return this.options.numToasts;
    }

    set width(value) {
        this.options.width = value;
    }

    get width() {
        return this.options.width;
    }

    set duration(value) {
        this.options.duration = value;
    }

    get duration() {
        return this.options.duration;
    }

    set timing(value) {
        this.options.timing = value;
    }

    get timing() {
        return this.options.timing;
    }

    set dimOld(value) {
        this.options.dimOld = value;
    }

    get dimOld() {
        return this.options.dimOld;
    }

}

The class consists of methods that utilize CSS transitions and the transform property to output the notifications seamlessly. It's to improve the user experience and keep up to date with modern implementations.

Tip When we create a new instance of the class, we can create as many toasts as we desire because they'll stack coherently.

That's the entire source code for the toast notifications JS library. Strict methods are in place to ensure the code is of a high standard. With only 252 lines of code, the library is optimized for all web apps, including responsive apps.

4. How To Use

To create a new instance of the toasts class, we can execute the following code:

JS
const toasts = new Toasts();

Create a new instance with predefined options:

JS
const toasts = new Toasts({
    offsetX: 20, // 20px
    offsetY: 20, // 20px
    gap: 20, // The gap size in pixels between toasts
    width: 300, // 300px
    timing: 'ease', // See list of available CSS transition timings
    duration: '.5s', // Transition duration
    dimOld: true, // Dim old notifications while the newest notification stays highlighted
    position: 'top-right' // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
});

We can then subsequently create a new toast notification with:

JS
toasts.push({
    title: 'My Toast Notification Title',
    content: 'My toast notification description.'
});

If we want to create a toast notification that represents a success message, we can do:

JS
toasts.push({
    title: 'Success Toast',
    content: 'My notification description.',
    style: 'success'
});

Or if you want to create a toast notification that represents an error message, we can do:

JS
toasts.push({
    title: 'Error Toast',
    content: 'My notification description.',
    style: 'error'
});

If we want to create a toast notification that represents a verified message, we can do:

JS
toasts.push({
    title: 'Verified Toast',
    content: 'My notification description.',
    style: 'verified'
});

In addition to the above styles, we can add dark as a style option.

The toast object can accept open and close event handlers. To implement them, we can add them like so:

JS
toasts.push({
    title: 'Dark Toast',
    content: 'My notification description.',
    style: 'dark',
    onOpen: toast => {
        console.log('The toast has appeared!');
    },
    onClose: toast => {
        console.log('The toast has disappeared!');
    }
});

By default, the dismiss button will appear, but with the following code, we can disable it:

JS
toasts.push({
    title: 'Toast',
    content: 'My notification description.',
    closeButton: false
});

We can also automatically dismiss the toast after a specified duration:

JS
toasts.push({
    title: 'Toast',
    content: 'My notification description.',
    dismissAfter: '3s' // s = seconds
});

And last but not least, the following implementation will redirect to a particular URL when the user clicks the toast:

JS
toasts.push({
    title: 'Toast',
    content: 'Click me to visit CodeShack!',
    link: 'https://codeshack.io',
    linkTarget: '_blank'
});

The toast object accepts the following options:

Option Possible Values
Title [string]
Description [string]
style success
error
verified
dark
link [href]
linkTarget _blank
_self
_parent
_top
framename
dismissAfter [transition-duration]
closeButton [boolean]
onOpen [function]
onClose [function]
width [integer]

5. Example Source

The example source implements the code snippets mentioned in the how-to section.

HTML
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
        <meta name="viewport" content="width=device-width,minimum-scale=1">
		<title>Toast Notifications JS</title>
		<link href="style.css" rel="stylesheet" type="text/css">
        <style>
        * {
            box-sizing: border-box;
            font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
            font-size: 16px;
        }
        body {
            margin: 0;
            padding: 0;
            background-color: #f0f2f5;
        }            
        </style>
	</head>
	<body>
        <script src="Toasts.js"></script>
        <script>
        const toasts = new Toasts({
            width: 300,
            timing: 'ease',
            duration: '.5s',
            dimOld: false,
            position: 'top-right' // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
        });

        toasts.push({
            title: 'Dark Toast',
            content: 'Click me to visit CodeShack!',
            style: 'dark',
            closeButton: false,
            link: 'https://codeshack.io',
            linkTarget: '_blank',
            onOpen: toast => {
                console.log(toast);
            },
            onClose: toast => {
                console.log(toast);
            }
        });

        toasts.push({
            title: 'Success Toast',
            content: 'My notification description.',
            style: 'success'
        });

        toasts.push({
            title: 'Verified Toast',
            content: 'My notification description.',
            style: 'verified'
        });

        toasts.push({
            title: 'Error Toast',
            content: 'My notification description.',
            style: 'error'
        });

        toasts.push({
            title: 'Toast',
            content: 'My notification description.'
        });

        // Press SPACE to add a custom toast
        window.onkeyup = event => {
            if (event.key == ' ') {
                toasts.push({
                    title: 'Custom ' + (toasts.numToasts+1),
                    content: 'Custom description ' + (toasts.numToasts+1) + '.'
                });
            }
        };
        </script>
    </body>
</html>

The above will produce the following:

Toast Notifications Interface

Conclusion

The whole purpose of the toast notifications library is to provide an innovative and pleasant experience to your website's visitors without the need for a fully-fledged library such as Bootstrap, which can negatively impact the performance of your web app and only has limited customizable options available.

If you've enjoyed reading this article, don't forget to share your thoughts in the comments section below and share it using the social links. It would be much appreciated!

Released under the MIT license.