Toast notifications provide brief, non-intrusive feedback about an operation through a small popup. They automatically disappear after a short time and don't require user interaction.
import {
NotificationsContainer,
type NotificationsService
} from 'frontile';
Note: In most applications, you should place a single
NotificationsContainerin your application template rather than including it in individual components. This prevents conflicts and provides a consistent notification experience across your app.
For best results, place a single NotificationsContainer in your application template:
{{! app/templates/application.hbs }}
<div id='app-content'>
{{outlet}}
</div>
{{! Global notifications container }}
<NotificationsContainer @placement='bottom-right' />
This approach ensures:
The simplest way to show a notification is to inject the notifications service and call the add method.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { Button } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class BasicExample extends Component {
@service notifications!: NotificationsService;
showNotification = () => {
this.notifications.add('This is a basic notification!');
};
<template>
<Button @onPress={{this.showNotification}}>
Show Notification
</Button>
</template>
}
Use different appearances to convey the appropriate message type.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { Button } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class AppearanceExample extends Component {
@service notifications!: NotificationsService;
showInfo = () => {
this.notifications.add('This is an info notification', {
appearance: 'info'
});
};
showSuccess = () => {
this.notifications.add('Operation completed successfully!', {
appearance: 'success'
});
};
showWarning = () => {
this.notifications.add('Please check your input', {
appearance: 'warning'
});
};
showError = () => {
this.notifications.add('Something went wrong', {
appearance: 'error'
});
};
<template>
<div class='grid grid-cols-2 gap-2'>
<Button @onPress={{this.showInfo}}>Info</Button>
<Button @onPress={{this.showSuccess}} @intent='success'>Success</Button>
<Button @onPress={{this.showWarning}} @intent='warning'>Warning</Button>
<Button @onPress={{this.showError}} @intent='danger'>Error</Button>
</div>
</template>
}
Control where notifications appear on screen with the @placement argument.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { Button } from 'frontile';
import { RadioGroup } from 'frontile';
import { NotificationsContainer, type NotificationsService } from 'frontile';
export default class PositionExample extends Component {
@service notifications!: NotificationsService;
@tracked placement = 'bottom-right';
placements = [
{ key: 'top-left', label: 'Top Left' },
{ key: 'top-center', label: 'Top Center' },
{ key: 'top-right', label: 'Top Right' },
{ key: 'bottom-left', label: 'Bottom Left' },
{ key: 'bottom-center', label: 'Bottom Center' },
{ key: 'bottom-right', label: 'Bottom Right' }
];
setPlacement = (placement: string) => {
this.placement = placement;
};
showNotification = () => {
this.notifications.add(`Positioned at ${this.placement}`, {
appearance: 'info'
});
};
<template>
<div class='flex flex-col gap-4'>
<RadioGroup
@label='Position'
@value={{this.placement}}
@onChange={{this.setPlacement}}
as |Radio|
>
{{#each this.placements as |option|}}
<Radio @value={{option.key}} @label={{option.label}} />
{{/each}}
</RadioGroup>
<Button @onPress={{this.showNotification}}>
Show at
{{this.placement}}
</Button>
<NotificationsContainer @placement={{this.placement}} />
</div>
</template>
}
Add action buttons to notifications for user interaction.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { Button } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class ActionsExample extends Component {
@service notifications!: NotificationsService;
@tracked result = '';
showWithActions = () => {
this.result = '';
this.notifications.add('File uploaded successfully!', {
appearance: 'success',
duration: 10000, // Keep it open longer for user to act
customActions: [
{
label: 'View',
onClick: () => {
this.result = 'User clicked View';
}
},
{
label: 'Share',
onClick: () => {
this.result = 'User clicked Share';
}
}
]
});
};
showUndoAction = () => {
this.result = '';
this.notifications.add('Item deleted', {
appearance: 'info',
duration: 8000,
customActions: [
{
label: 'Undo',
onClick: () => {
this.result = 'Undo clicked - item restored!';
}
}
]
});
};
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.showWithActions}}>
Upload Success with Actions
</Button>
<Button @onPress={{this.showUndoAction}}>
Show Undo Action
</Button>
</div>
{{#if this.result}}
<div
class='p-3 rounded border bg-success-subtle text-success-strong border-success-muted'
>
{{this.result}}
</div>
{{/if}}
</div>
</template>
}
Use preserve: true to prevent automatic dismissal.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { Button } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class PersistentExample extends Component {
@service notifications!: NotificationsService;
showPersistent = () => {
this.notifications.add('This notification stays until manually closed', {
appearance: 'warning',
preserve: true
});
};
showWithoutCloseButton = () => {
this.notifications.add('No close button - click actions to dismiss', {
appearance: 'info',
preserve: true,
allowClosing: false,
customActions: [
{
label: 'Got it',
onClick: () => {
// This will dismiss the notification
}
}
]
});
};
<template>
<div class='flex gap-2'>
<Button @onPress={{this.showPersistent}}>
Persistent Notification
</Button>
<Button @onPress={{this.showWithoutCloseButton}}>
No Close Button
</Button>
</div>
</template>
}
Track notification dismissals for analytics, backend updates, or cleanup. When using a global container, set up callbacks in your application template:
{{! app/templates/application.hbs }}
<NotificationsContainer
@placement='bottom-right'
@onDismiss={{this.handleNotificationDismissed}}
/>
// app/controllers/application.ts
import Controller from '@ember/controller';
export default class ApplicationController extends Controller {
handleNotificationDismissed = (notification) => {
if (notification.metadata) {
// Send analytics event
this.analytics.track('notification_dismissed', notification.metadata);
// Mark as read on backend
if (notification.metadata.notificationId) {
this.api.markNotificationAsRead(notification.metadata.notificationId);
}
}
};
}
Then from any component, add notifications with metadata:
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { Button } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class CallbackExample extends Component {
@service notifications!: NotificationsService;
showWithCallback = () => {
this.notifications.add('Notification with tracking', {
appearance: 'info',
metadata: {
id: `notification_${Date.now()}`,
source: 'demo',
action: 'user_interaction'
}
});
};
<template>
<Button @onPress={{this.showWithCallback}}>
Show Tracked Notification
</Button>
</template>
}
Control timing for individual notifications.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { Button } from 'frontile';
import { Input } from 'frontile';
import type { NotificationsService } from 'frontile';
export default class TimingExample extends Component {
@service notifications!: NotificationsService;
@tracked duration = 3000;
showShortDuration = () => {
this.notifications.add('Quick notification (1 second)', {
appearance: 'info',
duration: 1000
});
};
showLongDuration = () => {
this.notifications.add('Long notification (10 seconds)', {
appearance: 'warning',
duration: 10000
});
};
showCustomDuration = () => {
this.notifications.add(`Custom duration (${this.duration}ms)`, {
appearance: 'success',
duration: this.duration
});
};
updateDuration = (value) => {
this.duration = parseInt(value) || 3000;
};
<template>
<div class='flex flex-col gap-4'>
<Input
@label='Duration (ms)'
@type='number'
@value={{this.duration}}
@onInput={{this.updateDuration}}
/>
<div class='flex gap-2'>
<Button @onPress={{this.showShortDuration}}>
1 Second
</Button>
<Button @onPress={{this.showLongDuration}}>
10 Seconds
</Button>
<Button @onPress={{this.showCustomDuration}}>
Custom Duration
</Button>
</div>
</div>
</template>
}
Use TypeScript generics for strongly-typed metadata.
interface UserActionMetadata {
userId: string;
action: 'upload' | 'delete' | 'share';
resourceId: string;
timestamp: string;
}
// Type-safe notification with metadata
const notification = this.notifications.add<UserActionMetadata>(
'File uploaded successfully',
{
appearance: 'success',
metadata: {
userId: '123',
action: 'upload',
resourceId: 'file-456',
timestamp: new Date().toISOString()
}
}
);
// notification.metadata is now strongly typed as UserActionMetadata
export default class NotificationService extends Component {
@service notifications;
@service api;
async showNotificationWithBackendSync() {
const notification = this.notifications.add('New message received', {
appearance: 'info',
metadata: {
notificationId: 'msg_123',
userId: this.currentUser.id,
source: 'message_system'
}
});
}
handleNotificationDismissed = async (notification) => {
if (notification.metadata?.notificationId) {
// Mark as read on backend
await this.api.markNotificationAsRead(
notification.metadata.notificationId
);
// Track analytics
this.analytics.track('notification_dismissed', {
notificationId: notification.metadata.notificationId,
userId: notification.metadata.userId,
dismissedAt: new Date().toISOString()
});
}
};
}
All options available when creating notifications:
appearance: Visual style ('info' | 'success' | 'warning' | 'error')duration: Auto-dismiss time in milliseconds (default: 5000)preserve: Prevent auto-dismissal (default: false)allowClosing: Show close button (default: true)transitionDuration: Animation duration in milliseconds (default: 200)customActions: Array of action buttons with labels and onClick handlersmetadata: Custom data attached to the notificationOptions for the NotificationsContainer component:
placement: Position on screen (default: 'bottom-right')
'top-left' | 'top-center' | 'top-right''bottom-left' | 'bottom-center' | 'bottom-right'spacing: Gap between notifications in pixels (default: 16)onDismiss: Callback function called when notifications are dismissedclass: Custom CSS classes for stylingThe notification system includes built-in accessibility features:
role="alert", aria-live="assertive", and aria-atomic="true"The notifications service manages the global notification state and provides methods for adding and removing notifications.
| Method | Parameters | Return Type | Description |
|---|---|---|---|
add<TMetadata> |
message: string, options?: NotificationOptions<TMetadata> |
Notification<TMetadata> |
Adds a new notification with optional metadata and configuration |
remove |
notification?: Notification |
void |
Removes a specific notification |
removeAll |
- | void |
Removes all current notifications |
setOnRemoveCallback |
callback?: (notification: Notification) => void |
void |
Sets a global callback for when notifications are dismissed |
| Property | Type | Description |
|---|---|---|
notifications |
Notification[] |
Array of current notifications (read-only) |
Configuration options when creating notifications:
| Option | Type | Default | Description |
|---|---|---|---|
appearance |
'info' | 'success' | 'warning' | 'error' |
'info' |
Visual style of the notification |
duration |
number |
5000 |
Auto-dismiss time in milliseconds |
preserve |
boolean |
false |
Prevent auto-dismissal |
allowClosing |
boolean |
true |
Show close button |
transitionDuration |
number |
200 |
Animation duration in milliseconds |
customActions |
CustomAction[] |
undefined |
Array of action buttons |
metadata |
TMetadata |
undefined |
Custom data attached to notification |
Structure for notification action buttons:
| Property | Type | Description |
|---|---|---|
label |
string |
Text displayed on the button |
onClick |
() => void |
Function called when button is clicked |
Available positions for the notifications container:
'top-left' - Top left corner'top-center' - Top center'top-right' - Top right corner'bottom-left' - Bottom left corner'bottom-center' - Bottom center'bottom-right' - Bottom right corner (default)Element: <span class="hljs-title class_">HTMLDivElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Custom class name, it will override the default ones using Tailwind Merge library. |
onDismiss
|
function
|
- | Callback called when a notification is dismissed |
placement
|
enum
|
'bottom-right'
|
The placement of the notifications |
spacing
|
number
|
16
|
Spacing for each notification, in px. |