@svelte-put/noti GitHub

type-safe and headless async notification builder

@svelte-put/noti @svelte-put/noti @svelte-put/noti @svelte-put/noti changelog



npm install --save-dev @svelte-put/noti

Comprehensive Example

This section presents a working example of the package. You will notice that @svelte-put/noti only handles the core logics and leave UI up to you to configure. And for that reason, the setup can be a bit verbose. However, this enables flexibility. We will unpack each part of the library in later sections of the document to see how customization can be achieved.


Click to push toast notification

  • 1

    : a component is set up to be rendered as notification,

  • 2

    : a NotificationStore is created with (optionally) predefined notification variants,

  • 3

    : a "notification portal" is registered with use:portal, for rendering notifications,

  • 4

    : notification is "pushed" using the previously created NotificationStore.

a comprehensive example

<script lang="ts">
  import { notiStore } from './notification-store';

  const pushInfo = () => notiStore.push('info', { props: { content: 'An info notification' } });
  const pushSpecial = () => notiStore.push('special');

<!-- notification push triggers -->
<button class="c-btn-primary" on:click={pushInfo}>Push an info notification</button>
<button class="c-btn-primary-outline" on:click={pushSpecial}>Push a special notification</button>


The NotificationStore is the key part of @svelte-put/noti. It holds all internal logics and is used for the push & pop mechanism. As shown in the Comprehensive Example section, NotificationStore is created with a builder pattern that provides type-safety for push invocations.


notification store builder pattern

import { store } from '@svelte-put/noti';
const notiStore = store({ /** optional common config */ })
  .variant(/* */)
  .build(); // remember to call this to build the actual store

The store function accepts an optional config object that will be merged to all notification instance config on push.

simplified interfaces of store & NotificationCommonConfig

type store = (
  config?: NotificationCommonConfig,
) => import('@svelte-put/noti').NotificationStoreBuilder;

type NotificationCommonConfig = {
   * milliseconds to wait and automatically pop the notification.
   * Defaults to `3000`. Set to `false` to disable
  timeout?: number | false;
   * id generator for notifications. Defaults to 'uuid'.
   * @remarks
   *   - counter: use an auto-incremented counter that is scoped to the store
   *   - uuid: use `crypto.randomUUID()`, fallback to `counter` if not available
   *   - callback: a custom function that accepts {@link NotificationInstanceConfig} and returns a string as the id
    | 'counter'
    | 'uuid'
    | ((config: {
        /* NotificationInstanceConfig, omitted for conciseness */
      }) => string);


The variant method adds a predefined variant config that provides intellisense and reusability. It accepts a mandatory variant string, and either a Svelte component or a config object (see Comprehensive Example ).

simplified interfaces of variant & NotificationVariantConfig

type SvelteComponentClass = import('svelte').ComponentType<import('svelte').SvelteComponent>;

type variant = (
  variant: string,
  config: NotificationVariantConfig | SvelteComponentClass,
) => import('@svelte-put/noti').NotificationStoreBuilder;

type NotificationVariantConfig = {
  /** extends NotificationCommonConfig, omitted for conciseness */
  variant: string;
  component: SvelteComponentClass;
  props?: {
    /** inferred props for component */


New notifications can be pushed with the NotificationStore.push method. A push call can use either one of the predefined variant, as seen in the Comprehensive Example section, ...

push from predefined variant config

notiStore.push('<variant>', { /** optional config & component props */ });

... or the 'custom' variant, helpful for one-off notification for example.

Custom push must provide a component in its config.

push with custom config

notiStore.push('custom', {
  component: NotificationComponent, // required
  props: { /** props for NotificationComponent */ },
  id: () => 'one-time-id',
  timeout: false,

If you find that the push interface is too verbose (it is), you can further create your own proxy utils.

user-abstracted proxy push

export const info = (content: string) => notiStore.push('info', { props: { content } });
// later
info('An info notification...');

The API is intentionally kept verbose to maintain a unified interface that works for all use cases. But if you think it can be further simplified, feedback & proposal are welcomed 🙇.

Popping and Awaiting for Resolution

An active notification can be popped either:

  • from within the component (typically from user actions), by dispatching a resolve event (as seen in the Comprehensive Example section or demo below), or

  • via the pop method of NotificationStore,

    popping via NotificationStore

    import { notiStore } from './notification-store';
    // pop the topmost notification
    // pop a specific notification
    // pop a specific notification with custom resolution value
    notiStore.pop('id', 'custom-resolve-detail');
    // alternatively, provide arguments as object
      detail: 'custom-resolve-detail',
    }); // pop the topmost notification with custom resolution value

Notification resolution can be awaited. The awaited value is inferred from either the argument provided to NotificationStore.pop or CustomEvent.detail of the resolve event. This is especially helpful for complex interactive notification; see Notification Component section for an example.

awaiting for resolution

import { notiStore } from './notification-store';

const pushed = notiStore.push('info');
const resolved = await pushed.resolve();

demo: popping & awaiting

<script lang="ts">
  import { notiStore } from './notification-store';

  let promise: Promise<unknown> | null = null;
  async function pushNoti() {
    const pushed = notiStore.push('info', {
      timeout: false,
      props: {
        content: 'A persistent notification',

    promise = pushed.resolve();
    await promise;
    setTimeout(() => (promise = null), 2000);

  function popNoti() {

  Push a persistent notification
{#if promise}
  <p class="mt-2 text-blue-500">
    {#await promise}
      Notification is pushed and waiting for resolution. Either click the x button on the
      notification, or <button class="c-btn-text" on:click={popNoti}>click here</button> to pop the notification.
      Resolved (resetting in 2 seconds)

Timeout and Progress

If your notification has timeout specified in its config, a setTimeout is setup and the notification will be automatically popped from the stack. This timeout can be paused and resumed.

NotificationStore - progress control


The pause and resume methods on NotificationStore are actually just proxy methods for the same ones on NotificationInstance, which is accessible from within the Notification component via the injected notification prop .

NotificationInstance - progress control

<script lang="ts">
  import type { NotificationInstance } from '@svelte-put/noti';

  export let notification: NotificationInstance;

  const { progress } = notification;

  $: console.log($progress.state); // 'idle' | 'running' | 'paused' | 'ended'
  const pause = () => progress.pause();
  const resume = () => progress.resume();

Notification Portal


The accompanying portal svelte action provides a quick & minimal solution to set any HTMLElement as the rendering portal for a NotificationStore .

When using the portal action, only one portal can be bound to a NotificationStore, and vice versa.

Portal action

import { portal } from 'svelte-put/noti';
<div use:portal={notiStore} />

Notification instances are rendered as direct children of the HTMLElement use:portal is attached to. Newest instance is the last child.


use:portal is helpful for reducing boilerplate and keeping everything connected. However, there are some known UI limitations:

  • transition for the notification component must be global (for example in:fly|global),
  • outro transition (during unmount) will not run (but soon will be able to when this PR is merged),
  • animate is not available because it requires a keyed each block.

The next section discusses how a custom portal can be built to overcome these limitations, should it be necessary.

Custom Portal

Instead of use:portal , rendering of notifications can be manually handled by subscribing to the notifications array property of a NotificationStore . This is helpful when more granular control over rendering is necessary. For example, to coordinate and animate smoothly the positions of the notifications, as done in the following demo.


Click to push toast notification

custom portal demo

<script lang="ts">
  import { store } from '@svelte-put/noti';
  import NotificationWrapper from '@svelte-put/noti/Notification.svelte';
  import { flip } from 'svelte/animate';
  import { fly, fade } from 'svelte/transition';

  import Notification from './Notification.svelte';

  // define somewhere global, reuse across app
  const notiStore = store()
    .variant('info', Notification)
    .variant('special', {
      id: 'counter',
      component: Notification,
      props: {
        special: true,
        content: 'A very special notification',

  const pushInfo = () => notiStore.push('info', { props: { content: 'An info notification' } });
  const pushSpecial = () => notiStore.push('special');

<!-- notification portal, typically setup at somewhere global like root layout -->
  class="fixed z-notification inset-y-0 right-0 md:left-1/2 p-10 pointer-events-none flex flex-col-reverse gap-4 justify-end md:justify-start"
  {#each $notiStore.notifications as notification (notification.id)}
    <div animate:flip={{ duration: 200 }} in:fly={{ duration: 200 }} out:fade={{ duration: 120 }}>
      <NotificationWrapper {notification} />

<!-- notification push triggers -->
<button class="c-btn-primary" on:click={pushInfo}>Push an info notification</button>
<button class="c-btn-primary-outline" on:click={pushSpecial}>Push a special notification</button>

Notice the usage of @svelte-put/noti/Notification.svelte component above. It is just a small abstraction on top of svelte:component to provide the same functionality that use:portal does.
You can even go more granular and omit it; just make sure to provide the necessary props .

Notification Component

There is no limitation on the Svelte component to be used with @svelte-put/noti. However, this section lists some optional prop & event interfaces that helps build feature-rich notifications.

Injected notification Prop

This is an optional prop that provides access to the corresponding NotificationInstance (element of notification stack from NotificationStore).

simplified NotificationInstance interface

type NotificationInstanceConfig = {
  /** extends NotificationVariantConfig, omitted for conciseness */
  id: string;

type NotificationInstance = NotificationInstanceConfig & {
  /** reference to the rendered notification component */
  instance?: SvelteComponent;
  /** internal api for resolving a notification, effectively popping it from the stack */
  $resolve: (e: ComponentEvents['resolve']) => Promise<ComponentEvents['resolve']['detail']>;
  /** svelte store with .pause & .resume methods for controlling automatic timeout */
  progress: NotificationProgressStore;

This is helpful, for example, if you want access to the id or variant of the pushed notification.

use case for injected notification prop

<!-- SomeNotificationComponent.svelte -->
<script lang="ts">
  import type { NotificationInstance } from '@svelte-put/noti';

  export let notification: NotificationInstance;

<div data-id={notification.id} class="notification notification--{notification.variant}" />

The notification prop also allows access to the progress store for controlling timeout. Check Timeout and Progress for more information. Also refer to the Notification component used in Comprehensive Example which made use of the progress store to pause notification timeout on mouse hover.

resolve Event

If set up correctly, either automatically via use:portal or manually in your custom portal , a resolve event dispatched from the pushed instance will prompt NotificationStore to remove it from the current notification stack.

The detail of this resolve CustomEvent can be awaited, allowing us to receive user actions from complex interactive notifications such as the example below.


Waiting for notification to be pushed

interactive notification

  import InteractiveNotification from './InteractiveNotification.svelte';
  import { notiStore } from './notification-store';

  let state = 'idle';
  async function pushNoti() {
    const pushed = notiStore.push('custom', {
      timeout: false,
      component: InteractiveNotification,
      props: {
        message: 'You are invited to join the Svelte community!',

    state = 'pending';
    const agreed = await pushed.resolve();
    state = agreed ? 'accepted' : 'denied';

<p class="text-blue-500">
  {#if state === 'idle'}
    Waiting for notification to be pushed
  {:else if state === 'pending'}
    Waiting for user action to resolve notification
    Invitation was <strong
      class:text-red-500={state == 'denied'}
      class:text-green-500={state === 'accepted'}
<button class="c-btn-primary" on:click={pushNoti} disabled={state === 'pending'}
  >Trigger Interactive Notification</button
mouse click faster

Happy notifying! 👨‍💻

Edit this page on GitHub