# Beautiful toast notifications for Livewire
[](https://packagist.org/packages/masmerise/livewire-toaster)
[](https://github.com/masmerise/livewire-toaster/actions?query=workflow%3A%22Automated+testing%22+branch%3Amaster)
[](https://packagist.org/packages/masmerise/livewire-toaster)
**Toaster** provides a seamless experience to display toast notifications in your Livewire powered Laravel apps.
Unlike many other toast implementations that are available, Toaster makes it effortless to dispatch a toast notification
from either a standard `Controller` or a Livewire `Component`. You don't have to think about "flashing" things to the
session or "dispatching browser events" from your Livewire components. Just dispatch your toast and Toaster will route the message accordingly.
## Showcase
_* feature complete_
## Contents
**Looking for v1 docs?** [Click here](https://github.com/masmerise/livewire-toaster/tree/1.3.0).
- [Installation](#installation)
- [Preparing your template](#preparing-your-template)
- [Configuring scripts](#configuring-scripts)
- [Tailwind styles](#tailwind-styles)
- [RTL support](#rtl-support)
- [Usage](#usage)
- [Sending toasts from the back-end](#sending-toasts-from-the-back-end)
- [Sending toasts from the front-end](#sending-toasts-from-the-front-end)
- [Automatic translation of messages](#automatic-translation-of-messages)
- [Accessibility](#accessibility)
- [Replacing similar toasts](#replacing-similar-toasts)
- [Suppressing duplicate toasts](#suppressing-duplicate-toasts)
- [Unit testing](#unit-testing)
- [Extending behavior](#extending-behavior)
- [View customization](#view-customization)
- [Testing](#testing)
- [Changelog](#changelog)
- [Security](#security)
- [Credits](#credits)
- [License](#license)
## Installation
You can install the package via [composer](https://getcomposer.org):
```bash
composer require masmerise/livewire-toaster
```
You can publish the package's config file:
```bash
php artisan vendor:publish --tag=toaster-config
```
This is the contents of the `toaster.php` config file:
```php
return [
/**
* Add an additional second for every 100th word of the toast messages.
*
* Supported: true | false
*/
'accessibility' => true,
/**
* The vertical alignment of the toast container.
*
* Supported: "bottom", "middle" or "top"
*/
'alignment' => 'bottom',
/**
* Allow users to close toast messages prematurely.
*
* Supported: true | false
*/
'closeable' => true,
/**
* The on-screen duration of each toast.
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 3000,
/**
* The horizontal position of each toast.
*
* Supported: "center", "left" or "right"
*/
'position' => 'right',
/**
* New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.
* Takes precedence over the "suppress" option.
*
* Supported: true | false
*/
'replace' => false,
/**
* Prevent the display of duplicate toast messages.
*
* Supported: true | false
*/
'suppress' => false,
/**
* Whether messages passed as translation keys should be translated automatically.
*
* Supported: true | false
*/
'translate' => true,
];
```
### Preparing your template
Next, you'll need to use the `` component in your master template:
```html
```
### Configuring scripts
After that, you'll need to import `Toaster` at the top of your `resources/js/app.js` bundle to start listening to incoming toasts:
```js
import './bootstrap';
import '../../vendor/masmerise/livewire-toaster/resources/js'; // 👈
// other app stuff...
```
### Tailwind styles
> [!NOTE]
> Skip this step if you're going to customize Toaster's default view.
Toaster provides a minimal view that utilizes Tailwind CSS defaults.
If the default toast appearances suffice your needs, you'll need to register it with Tailwind's purge list:
For Tailwind CSS Version < 4.x
```js
module.exports = {
content: [
'./resources/**/*.blade.php',
'./vendor/masmerise/livewire-toaster/resources/views/*.blade.php', // 👈
],
}
```
For Tailwind CSS V >= 4+ you'll need to add this to your `app.css` file:
> [!NOTE]
> Tailwind CSS v4.0 introduced a major change where you define your source content files directly in the main CSS entry point using the `@source` directive. This is the **most modern and recommended approach** for v4+.
```css
@import "tailwindcss";
...
@source '../../vendor/masmerise/livewire-toaster/resources/views/*.blade.php'; /* 👈 */
```
or into the file `tailwind.config.js`:
> [!NOTE]
> The `content` array still exists and functions in Tailwind CSS 4+. For many existing projects or frameworks, defining the paths here is a fallback or continued method. The config file itself is correctly updated to use the modern ESM `export default` syntax.
```js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
"./vendor/masmerise/livewire-toaster/resources/views/*.blade.php", // 👈
],
plugins: [],
};
```
Otherwise, please refer to [View customization](#view-customization).
### RTL support
> [!NOTE]
> **LTR** will be assumed regardless of whether you apply the `ltr` attribute or not.
If your app makes use of an **RTL** language such as Arabic and Hebrew, don't forget to add the `rtl` attribute to the document root:
```html
...
```
This will make sure the UI elements (such as the close button) are flipped and the text is properly aligned.
## Usage
### Sending toasts from the back-end
> [!NOTE]
> Toaster supports the dispatch of multiple toasts at once, you are not limited to dispatching a single toast.
#### Toaster
The standard recommended way for dispatching toast messages is through the `Toaster` facade.
```php
use Masmerise\Toaster\Toaster;
final class RegistrationForm extends Component
{
public function submit(): void
{
$this->validate();
User::create($this->form);
Toaster::success('User created!'); // 👈
}
}
```
If you need fine-grained control, you can always use the `PendingToast` class directly to which `Toaster` proxies its calls:
```php
use Masmerise\Toaster\PendingToast;
final class RegistrationForm extends Component
{
public function submit(): void
{
$this->validate();
$user = User::create($this->form);
// 👇
PendingToast::create()
->when($user->isAdmin(),
fn (PendingToast $toast) => $toast->message('Admin created')
)
->unless($user->isAdmin(),
fn (PendingToast $toast) => $toast->message('User created')
)
->success();
}
}
```
#### Toastable
You can make any class `Toastable` to dispatch toasts from:
```php
use Masmerise\Toaster\Toastable;
final class ProductListing extends Component
{
use Toastable; // 👈
public function check(): void
{
$result = Product::query()
->tap(new Available())
->count();
if ($result < 5) {
$this->warning('The quantity on hand is critically low.'); // 👈
}
}
}
```
#### Redirects
Whenever you return a `RedirectResponse` from anywhere in your app, you can chain any of the `Toaster` methods
to dispatch a toast message:
```php
final class CompanyController extends Controller
{
/** @throws ValidationException */
public function store(Request $request): RedirectResponse
{
$validator = Validator::make($request->all(), [...]);
if ($validator->fails()) {
return Redirect::back()
->error('The form contains several errors'); // 👈
}
Company::create($validator->validate());
return Redirect::route('dashboard')
->info('Company created!'); // 👈
}
}
```
This is, of course, **not** limited to `Controller`s as you can also redirect in Livewire `Component`s.
#### Dependency injection
If you'd like to keep things "pure", you can also inject the `Collector` contract
and use the `ToastBuilder` to dispatch your toasts:
```php
use Masmerise\Toaster\Collector;
use Masmerise\Toaster\ToasterConfig;
use Masmerise\Toaster\ToastBuilder;
final readonly class SendEmailVerifiedNotification
{
public function __construct(
private ToasterConfig $config,
private Collector $toasts,
) {}
public function handle(Verified $event): void
{
$toast = ToastBuilder::create()
->duration($this->config->duration)
->success()
->message("Thank you, {$event->user->name}!")
->get();
$this->toasts->collect($toast);
}
}
```
### Sending toasts from the front-end
You can invoke the globally available `Toaster` instance to dispatch any toast message from anywhere:
```html
```
Available methods: `error`, `info`, `warning` & `success`
### Automatic translation of messages
> [!NOTE]
> The `translate` configuration value must be set to `true`.
Instead of doing this:
```php
Toaster::success(
Lang::get('path.to.translation', ['replacement' => 'value'])
);
```
Toaster makes it possible to do this:
```php
Toaster::success('path.to.translation', ['replacement' => 'value']);
```
You can mix and match without any problems:
```php
Toaster::info('user.created', ['name' => $user->full_name]);
Toaster::info('You now have full access!');
```
You can do whatever you want, whenever you want.
### Accessibility
> [!NOTE]
> The `accessibility` configuration value must be set to `true`.
Toaster will add an additional second to a toast's on-screen duration for every 100th word.
This way, your users will have enough time to read toasts that are a tad larger than usual.
So, if your base duration value is `3 seconds` and your toast contains 223 words,
the total on-screen duration of the toast will be `3 + 2 = 5 seconds`
### Replacing similar toasts
> [!NOTE]
> The `replace` configuration value must be set to `true`.
> [!WARNING]
> Takes precedence over `suppress`.
Toaster will dispose of any toast that is similar to the one being dispatched prior to displaying the new toast.
A toast is considered similar if it has the same `duration`, `message`, and `type`.
### Suppressing duplicate toasts
> [!NOTE]
> The `suppress` configuration value must be set to `true`.
Toaster will prevent the display of duplicate toast messages while another toast with the same message is still on-screen.
A toast is considered a duplicate if it has the same `duration`, `message`, and `type`.
### Unit testing
> [!NOTE]
> If you make use of [automatic translation of messages](#automatic-translation-of-messages), you should assert whether the **translation keys** are passed along correctly instead of the human readable messages that are replaced by Laravel's translator.
> Otherwise, your tests are going to fail as the messages are not translated during unit testing.
Toaster provides a couple of testing capabilities in order for you to build a robust application:
```php
use Masmerise\Toaster\Toaster;
final class RegisterUserControllerTest extends TestCase
{
#[Test]
public function users_can_register(): void
{
// Arrange
Toaster::fake();
Toaster::assertNothingDispatched();
// Act
$response = $this->post('users', [ ... ]);
// Assert
$response->assertRedirect('profile');
Toaster::assertDispatched('Welcome!');
}
}
```
### Extending behavior
Imagine that you'd like to keep track of how many toasts are dispatched daily to display on an admin dashboard.
First, create a new class that encapsulates this logic:
```php
final readonly class DailyCountingCollector implements Collector
{
public function __construct(private Collector $next) {}
public function collect(Toast $toast): void
{
// increment the counter on durable storage
$this->next->collect($toast);
}
public function release(): array
{
return $this->next->release();
}
}
```
After that, extend the behavior in your `AppServiceProvider`:
```php
public function register(): void
{
$this->app->extend(Collector::class,
static fn (Collector $next) => new DailyCountingCollector($next)
);
}
```
That's it!
## View customization
Even though the default toasts are pretty, they might not fit your design and you may want to customize them.
You can do so by publishing Toaster's views:
```php
php artisan vendor:publish --tag=toaster-views
```
The `hub.blade.php` view will be published to your application's `resources/views/vendor/toaster` directory.
Feel free to modify anything to your liking.
### Available `viewData`
- `$alignment` - can be used to align the toast container vertically depending on the configuration
- `$closeable` - whether the close button should be rendered by the Blade component
- `$config` - default configuration values, used by the Alpine component
- `$position` - can be used to position the toasts depending on the configuration
- `$toasts` - toasts that were flashed to the session by Toaster, used by the Alpine component
> [!WARNING]
> You **must** keep the `x-data` and `x-init` directives and you **must** keep using the `x-for` loop.
> Otherwise, the Alpine component that powers Toaster will start malfunctioning.
## Testing
```bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Security
If you discover any security related issues, please email support@muhammedsari.me instead of using the issue tracker.
## Credits
- [Muhammed Sari](https://github.com/masmerise)
- [Greg Korba](https://github.com/wirone)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
================================================
FILE: UPGRADING.md
================================================
# Upgrade Guide
## v1 → v2
### Minimum versions
The following dependency versions have been updated:
- The minimum Livewire version is now v3.0
### Adjusting the `Toaster` import
`Alpine` is now registered by Livewire automatically. Therefore, any direct `Alpine.plugin` or `Alpine.start` call will no longer work.
You should adjust your `app.js` JavaScript entry point to account for this change. An example:
```diff
import './bootstrap';
+ import '../../vendor/masmerise/livewire-toaster/resources/js';
- import Alpine from 'alpinejs';
- import Toaster from '../../vendor/masmerise/livewire-toaster/resources/js';
- Alpine.plugin(Toaster);
- window.Alpine = Alpine;
- Alpine.start();
```
================================================
FILE: composer.json
================================================
{
"name": "masmerise/livewire-toaster",
"description": "Beautiful toast notifications for Laravel / Livewire.",
"license": "MIT",
"keywords": [
"alert",
"laravel",
"livewire",
"toast",
"toaster"
],
"authors": [
{
"name": "Muhammed Sari",
"email": "support@muhammedsari.me",
"role": "Developer"
}
],
"homepage": "https://github.com/masmerise/livewire-toaster",
"require": {
"php": "~8.2 || ~8.3 || ~8.4 || ~8.5",
"illuminate/contracts": "^11.0 || ^12.0 || ^13.0",
"illuminate/http": "^11.0 || ^12.0 || ^13.0",
"illuminate/routing": "^11.0 || ^12.0 || ^13.0",
"illuminate/support": "^11.0 || ^12.0 || ^13.0",
"illuminate/view": "^11.0 || ^12.0 || ^13.0",
"livewire/livewire": "^3.0 || ^4.0"
},
"require-dev": {
"larastan/larastan": "^2.0 || ^3.1",
"laravel/pint": "^1.0",
"orchestra/testbench": "^9.0 || ^10.0 || ^11.0",
"phpunit/phpunit": "^10.0 || ^11.5.3 || ^12.5.12"
},
"conflict": {
"stevebauman/unfinalize": "*"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Masmerise\\Toaster\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests"
}
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"aliases": {
"Toaster": "Masmerise\\Toaster\\Toaster"
},
"providers": [
"Masmerise\\Toaster\\ToasterServiceProvider"
]
}
},
"scripts": {
"format": "vendor/bin/pint",
"larastan": "vendor/bin/phpstan analyse --memory-limit=2G",
"test": "vendor/bin/phpunit",
"verify": [
"@larastan",
"@test"
]
}
}
================================================
FILE: config/toaster.php
================================================
true,
/**
* The vertical alignment of the toast container.
*
* Supported: "bottom", "middle" or "top"
*/
'alignment' => 'bottom',
/**
* Allow users to close toast messages prematurely.
*
* Supported: true | false
*/
'closeable' => true,
/**
* The on-screen duration of each toast.
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 3000,
/**
* The horizontal position of each toast.
*
* Supported: "center", "left" or "right"
*/
'position' => 'right',
/**
* New toasts immediately replace similar ones, ensuring only one toast of a kind is visible at any time.
* Takes precedence over the "suppress" option.
*
* Supported: true | false
*/
'replace' => false,
/**
* Prevent the display of duplicate toast messages.
*
* Supported: true | false
*/
'suppress' => false,
/**
* Whether messages passed as translation keys should be translated automatically.
*
* Supported: true | false
*/
'translate' => true,
];
================================================
FILE: phpstan.neon.dist
================================================
includes:
- ./vendor/larastan/larastan/extension.neon
parameters:
paths:
- src
level: 9
ignoreErrors:
- identifier: missingType.generics
- identifier: missingType.iterableValue
- '#Cannot access offset .* on Illuminate\\Contracts\\Foundation\\Application#'
- '#Parameter \#.* of class Masmerise\\Toaster\\ToasterConfig constructor expects .*, mixed given.#'
- '#Trait Masmerise\\Toaster\\Toastable is used zero times and is not analysed.#'
================================================
FILE: phpunit.xml.dist
================================================
tests
================================================
FILE: pint.json
================================================
{
"preset": "psr12",
"rules": {
"blank_line_after_opening_tag": false,
"blank_line_before_statement": { "statements": ["return"] },
"braces": { "allow_single_line_closure": true },
"concat_space": { "spacing": "one" },
"declare_strict_types": true,
"method_argument_space": false,
"no_extra_blank_lines": { "tokens": ["curly_brace_block", "extra", "throw", "use"] },
"no_useless_else": true,
"not_operator_with_successor_space": true,
"ordered_imports": { "imports_order": ["class", "function", "const"], "sort_algorithm": "alpha" },
"single_line_empty_body": true,
"trailing_comma_in_multiline": true
}
}
================================================
FILE: resources/js/config.js
================================================
class Alignment {
static Top = 'top';
constructor(value) {
this.value = value;
}
isTop() {
return this.value === Alignment.Top;
}
}
export class Config {
constructor(alignment, duration, replace, suppress) {
this.alignment = new Alignment(alignment);
this.duration = duration;
this.replace = replace;
this.suppress = suppress;
}
static fromJson(data) {
return new Config(data.alignment, data.duration, data.replace, data.suppress);
}
}
================================================
FILE: resources/js/hub.js
================================================
import { Config } from './config';
import { Toast } from './toast';
export function Hub(Alpine) {
Alpine.data('toasterHub', (initialToasts, config) => {
config = Config.fromJson(config);
return {
_toasts: [],
get toasts() {
const toasts = this._toasts.filter(t => ! t.trashed);
if (this._toasts.length && ! toasts.length) {
this.$nextTick(() => { this._toasts = []; });
}
return toasts;
},
init() {
document.addEventListener('toaster:received', event => {
this.processToast(event);
});
initialToasts.map(Toast.fromJson).forEach(toast => this.show(toast));
},
processToast(data) {
const toast = Toast.fromJson({ duration: config.duration, ...data.detail });
if (config.replace) {
this.toasts.filter(t => t.equals(toast)).forEach(t => t.dispose());
} else if (config.suppress && this.toasts.some(t => t.equals(toast))) {
return;
}
this.show(toast);
},
show(toast) {
toast = Alpine.reactive(toast);
toast.runAfterDuration(toast => toast.dispose());
if (config.alignment.isTop()) {
this._toasts.unshift(toast);
} else {
this._toasts.push(toast);
}
},
}
});
}
================================================
FILE: resources/js/index.js
================================================
import { Hub } from './hub';
import * as Toaster from './toaster';
window.Toaster = Toaster;
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(Hub);
});
================================================
FILE: resources/js/toast.js
================================================
import { uuid41 } from './uuid41';
export class Toast {
constructor(duration, message, type) {
this.$el = null;
this.id = uuid41();
this.isVisible = false;
this.duration = duration;
this.message = message;
this.timeout = null;
this.trashed = false;
this.type = type;
}
static fromJson(data) {
return new Toast(data.duration, data.message, data.type);
}
dispose() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.isVisible = false;
if (this.$el) {
this.$el.addEventListener('transitioncancel', () => { this.trashed = true; })
this.$el.addEventListener('transitionend', () => { this.trashed = true; })
}
}
equals(other) {
return this.duration === other.duration
&& this.message === other.message
&& this.type === other.type;
}
runAfterDuration(callback) {
this.timeout = setTimeout(() => callback(this), this.duration);
}
select(config) {
return config[this.type];
}
show($el) {
this.$el = $el;
this.isVisible = true;
}
}
================================================
FILE: resources/js/toaster.js
================================================
const event = (message, type) => {
document.dispatchEvent(new CustomEvent('toaster:received', { detail: { message, type }}));
};
const error = message => event(message, 'error');
const info = message => event(message, 'info');
const success = message => event(message, 'success');
const warning = message => event(message, 'warning');
export { error, info, success, warning }
================================================
FILE: resources/js/uuid41.js
================================================
export function uuid41() {
let d = '';
while (d.length < 32) {
d += Math.random().toString(16).substring(2);
}
const vr = ((Number.parseInt(d.substring(16, 1), 16) & 0x3) | 0x8).toString(16);
return `${d.substring(0, 8)}-${d.substring(8, 4)}-4${d.substring(13, 3)}-${vr}${d.substring(17, 3)}-${d.substring(20, 12)}`;
}
================================================
FILE: resources/views/close-button.blade.php
================================================
================================================
FILE: resources/views/hub.blade.php
================================================