Vue3-Toastify in a Laravel/Inertia/Vue project

April 4th, 2023 by Philip Iezzi 7 min read
cover image

Need a full-featured toast notification component for your next Vue.js project? Don't look any further: Vue3-Toastify beats it all!

Previously I have tried Vue Toastification («Light, easy and beautiful toasts») and Mosha Vue Toastify («A light weight and fun Vue 3 toast or notification or snack bar») but they are both kind of outdated and abandoned projects with latest releases dating back to prehistoric 2021. Let me show you how to integrate great looking and easy to use toast notifications into your VILT stack!

I first started to build my own toast component but soon ended up struggling with challenging states and animations as those were my (minimal) requirements:

  • Support multiple toasts
  • Autoclose with progress bar (success)
  • No auto-close on warning/error
  • Styled for info/success/warning/error notifications, playing nicely with Tailwind
  • Close on click
  • Pause on hover
  • Newest on top
  • Bounce transition

Vue3-Toastify offers it all, built-in, fully customizable, and «beautiful by default»! The author is not lying – look at this great feature set and the demo. Even the documentation couldn't be more detailed and simply crystal clear!

But hey, it's just a toast! Does it deserve its own blogpost? Definitely! I am a huge fanboy of that component, how well it is designed and how easy it is to use. 👏

Integrate Vue3-Toastify into VILT

Let me now show you how to integrate Vue3-Toastify into a project built on VILT (Vue3, Inertia.js, Laravel, Tailwind) stack, bootstrapped with Jetstream.

Our goal is to fire flash messages from the backend, potentially more than one per request. The flash messages should be displayed in our frontend and stay open even when navigating to other pages. They should be easy closable, and the success messages should auto-close after some seconds.

Let's start by importing the CSS and loading the Vue3Toastify component in resources/js/app.js (stripped down to the bare metal):

app.js
import './bootstrap'
import 'vue3-toastify/dist/index.css'
import '../css/app.css'

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import Vue3Toastify from 'vue3-toastify'

createInertiaApp({
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(Vue3Toastify)
            .mount(el)
    },
})

Import vue3-toastify's CSS before your app.css, so you could override some styles there, e.g.:

app.css
:root {
    --toastify-toast-width: 400px;
    --toastify-color-warning: #ec9b00;
}

(see How to style docs for all available --toastify-* CSS variables)

You could now fire a toast message from anywhere in your JS code like this (see Usages):

import { toast } from 'vue3-toastify'

const options = {
  autoClose: 5000,
  type: toast.TYPE.SUCCESS,
  newestOnTop: true,
  theme: toast.THEME.COLORED,
  // and so on ...
} as ToastOptions;

// display toasts
const toastId = toast("Hello", options as ToastOptions);

//shortcut to different types
toast.success("Hello", options as ToastOptions);
toast.info("World", options as ToastOptions);
toast.warn(MyComponent, options as ToastOptions);
toast.error("Error", options as ToastOptions);

But we don't want to care about firing toasts in the frontend – just fire them whenever they pop in! Read on...

ToastNotifications Vue component

Let's now write our global ToastNotifications Vue component. First, create a composable resources/js/Composables/ToastNotifications.js with all the «business logic»:

ToastNotifications.js
import { ref, watch } from 'vue'
import { usePage } from '@inertiajs/vue3'
import { toast } from 'vue3-toastify'

const toasts = ref()

const reToasted = ref(false)

function fireToast(notification, sticky = false) {
    toast(notification.message, {
        toastId: notification.id,
        type: notification.type,
        newestOnTop: true,
        theme: toast.THEME.COLORED,
        autoClose: !sticky && (notification.type == 'success' ? 5000 : false),
        closeOnClick: !sticky,
        onClose: () => (reToasted.value = false),
    })
}

function fireToasts(sticky = false) {
    toasts.value?.forEach((notification) => fireToast(notification, sticky))
}

function toastAgain() {
    reToasted.value ? toast.remove() : fireToasts(true)
    reToasted.value = !reToasted.value
}

watch(
    // NOTE: Since Inertia.js 1.0.1, usePage() may return null initially.
    () => usePage()?.props?.flash.toasts,
    (newToasts) => {
        reToasted.value = false
        toasts.value = newToasts
        fireToasts()
    }
)

export function useToasts() {
    return {
        toasts,
        toastAgain,
        reToasted,
    }
}

This fires toast messages whenever the flash.toasts shared prop gets filled. To make sure we don't ever create duplicate toasts, let's give each toast a unique id. Whenever the watched usePage().props.flash.toasts changes, the new toasts get fired.

I am using the following helpers in bootstrap/helpers.php (loaded in composer.json's "autoload": { "files": [ "bootstrap/helpers.php" ], } where the toasts are getting added to the session:

helpers.php
if (! function_exists('toast')) {
    function toast(ToastType $type, string $message, ?RedirectResponse $response = null)
    {
        $toasts = session()->get('toasts', []);
        $toasts[] = [
            'id'      => Str::uuid(),
            'type'    => $type->value,
            'message' => $message,
        ];
        if ($response) {
            return  $response->with('toasts', $toasts);
        } else {
            session()->flash('toasts', $toasts);
        }
    }
}

if (! function_exists('toast_success')) {
    function toast_success(string $message)
    {
        return toast(ToastType::SUCCESS, $message);
    }
}

if (! function_exists('toast_warning')) {
    function toast_warning(string $message)
    {
        return toast(ToastType::WARNING, $message);
    }
}

if (! function_exists('toast_error')) {
    function toast_error(string $message)
    {
        return toast(ToastType::ERROR, $message);
    }
}

... and the related PHP enum – completely unnecessary but I like to favor enums over strings wherever I can:

ToastType.php
namespace App\Enums;

enum ToastType: string
{
    case SUCCESS = 'success';
    case WARNING = 'warning';
    case ERROR = 'error';
}

Now, to make the toasts available to your frontend, add them to the Inertia shared data in HandleInertiaRequests middleware:

HandleInertiaRequests.php
<?php

namespace App\Http\Middleware;

use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request)
    {
        return array_merge(parent::share($request), [
            'flash' => fn () => [
                'toasts'=> $request->session()->get('toasts'),
            ],
            // ...
        ]);
    }
}

And finally, build your ToastNotifications Vue component:

ToastNotifications.vue
<script setup>
import { BellAlertIcon, BellSlashIcon } from '@heroicons/vue/24/outline'
import { useToasts } from '@/Composables/ToastNotifications'

const { toasts, toastAgain, reToasted } = useToasts()
</script>

<template>
    <div>
        <button
            v-if="toasts"
            class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 hover:bg-gray-800 hover:text-white dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
            :class="{ 'animate-pulse': !reToasted }"
            @click="toastAgain()"
        >
            <component :is="reToasted ? 'BellSlashIcon' : 'BellAlertIcon'" class="h-5 w-5" />
        </button>
    </div>
</template>

You don't need to get that fancy and could also just load the toasts somewhere else without an extra Vue component, or at least without the <template> part. But this component provides you the following:

  • Shows nothing when there is no toast message
  • Shows you a nice BellAlertIcon button when there are any toasts available (open or already closed but still on the same page)
  • You can click on the bell-alert icon to re-open the previous messages again.
  • When toasts are re-opened, they are not auto-closed (see sticky flag in ToastNotifications.js function fireToast()), but for easy closing of all toasts, you can just click on the bell-slash icon instead of closing each toast message individually.

Just put this component into your app header or main layout:

<template>
    <!-- ... -->
    <ToastNotifications />

done. 🎉

Oh, and in case you're missing some import statements in my Vue code, I love on-demand component auto-importing with unplugin-vue-components!

Usage in your Controllers

In your backend controllers, you can now simply use:

toast_success('User successfully updated!');

Or how about extending the RedirectResponse in your AppServiceProvider.php?:

AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    // ...
    private function registerMacros()
    {
        RedirectResponse::macro(
            'withSuccess',
            fn (string $message) => toast(ToastType::SUCCESS, $message, $this)
        );

        RedirectResponse::macro(
            'withError',
            fn (string $message) => toast(ToastType::ERROR, $message, $this)
        );
    }

Like this, you could just append the message to the redirect response:

    public function update(UpdateCustomerRequest $request, Customer $customer)
    {
        $customer->update($request->validated());
        return back()->withSuccess('The customer was fully pimped.');
    }

I wish you a perfectly toasted 🍞 slice of bread that never falls on the wrong side! If it does, just put a ! in front.