PxlKitToastProvider

App-root toast provider that hosts the toast queue, viewport portal, and stacked/expanded visual mode — paired with useToast() (injectToast() in Angular) for imperative push/update/dismiss/promise APIs.

  • Six positions (top/bottom × left/right/center) with portal-rendered viewport.
  • Sonner-style stacked mode: collapsed cards peek behind the front, hover/focus expands the stack.
  • Configurable max simultaneous toasts; oldest are dropped when the queue exceeds the cap.
  • Surface-aware (auto / pixel / linear) — pixel surface adds an HP-bar tone accent to each toast.
  • Announced through two persistent live regions, polite and assertive; F8 moves focus to the toasts, and duration sets or turns off their auto-dismiss.
Status
stable
Category
feedback
Since
v1.8.0

API

import { PxlKitToastProvider } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
children (required)React.ReactNode—The part of the app that shows toasts: useToast() works inside it.
position'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center''top-right'Corner, or edge centre, of the screen the toasts appear at.
maxnumber5Maximum simultaneous toasts. Oldest is dropped if exceeded. Defaults to 5.
durationnumber4500Auto-dismiss delay, in ms, of the toasts that set no duration of their own; 0 keeps them until dismissed. Defaults to 4500. A promise's error toast stays at least 6 s, unless this is 0.
hotkeystring | false'F8'Key that moves focus to the toasts, written like F8 or alt+t, or false for none. Defaults to F8; the viewport's accessible name tells it.
surface'pixel' | 'linear'—Surface of the toasts; defaults to the nearest provider.
stackedbooleantrueSonner-style stacked-offset visual: toasts collapse into a small stack showing only the front card; hover expands them into a vertical list. Defaults to true.
stackVisiblenumber2How many additional cards peek behind the front when stacked. Defaults to 2.

Accessibility

WCAG target: 2.1 AA

  • region
  • status
  • alert

The viewport is a role="region" landmark named after its hotkey — "Notifications (F8)" by default; hotkey sets another key or, with false, none — and takes focus from it (tabindex="-1"), from where Tab reaches the toasts' buttons. The hotkey works in text fields too, since F8 types nothing. Two visually hidden live regions inside the viewport, there and empty before any toast, announce the toasts: each toast's title and message are written to role="status" — or role="alert" for critical tones (red, gold) and assertive toasts — when it is pushed and when an update changes its title, message or tone (a settled promise), as new content each time, so a repeated message is read again. The cards themselves are not live regions. Toasts dismiss themselves after the provider's duration (4.5 s; 0 keeps them until dismissed, WCAG 2.2.1), a promise's error toast after at least 6 s; a toast's countdown holds while it is hovered or focused, while the page is hidden and while the window is in the background. Hovering or focusing the viewport expands the stacked layout. When the toast holding focus leaves, focus moves to the dismiss button of the next toast, else the previous one, else back to the element it entered the viewport from.

Keyboard

KeyActionWhen
F8Move focus to the toast viewport, expanding stacked toasts.Toasts on screen; another key, or none, with hotkey.
TabMove through the toasts' action and dismiss buttons, from the viewport or the page, expanding stacked toasts.—
Shift+TabMove focus back through the toasts and out of the viewport, collapsing the stack.—
EnterDismiss the toast; focus moves to the next toast's dismiss button, or the previous one's, or back to where it came from.Dismiss button focused.

Usage

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function DefaultTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton size="sm" onClick={() => toast({ title: 'Saved', message: 'Your changes were persisted.' })}>
        Push toast
      </PixelButton>
    </TriggerRow>
  );
}

export function Default() {
  return (
    <PxlKitToastProvider>
      <DefaultTriggers />
    </PxlKitToastProvider>
  );
}

Examples

Default

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function DefaultTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton size="sm" onClick={() => toast({ title: 'Saved', message: 'Your changes were persisted.' })}>
        Push toast
      </PixelButton>
    </TriggerRow>
  );
}

export function Default() {
  return (
    <PxlKitToastProvider>
      <DefaultTriggers />
    </PxlKitToastProvider>
  );
}

Tones

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function ToneTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton size="sm" tone="green" onClick={() => toast.success('Saved', 'Changes persisted.')}>
        Success
      </PixelButton>
      <PixelButton size="sm" tone="cyan" onClick={() => toast.info('Heads up', 'New release available.')}>
        Info
      </PixelButton>
      <PixelButton size="sm" tone="gold" onClick={() => toast.warning('Careful', 'Storage almost full.')}>
        Warning
      </PixelButton>
      <PixelButton size="sm" tone="red" onClick={() => toast.error('Failed', 'Upload could not finish.')}>
        Error
      </PixelButton>
    </TriggerRow>
  );
}

export function Tones() {
  return (
    <PxlKitToastProvider>
      <ToneTriggers />
    </PxlKitToastProvider>
  );
}

Bottom Right

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function PositionTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton size="sm" onClick={() => toast({ title: 'Bottom-right toast' })}>
        Push to bottom-right
      </PixelButton>
    </TriggerRow>
  );
}

export function BottomRight() {
  return (
    <PxlKitToastProvider position="bottom-right">
      <PositionTriggers />
    </PxlKitToastProvider>
  );
}

Top Center

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function PositionTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton size="sm" onClick={() => toast({ title: 'Bottom-right toast' })}>
        Push to bottom-right
      </PixelButton>
    </TriggerRow>
  );
}

export function TopCenter() {
  return (
    <PxlKitToastProvider position="top-center">
      <PositionTriggers />
    </PxlKitToastProvider>
  );
}

Stacked

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function StackedTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() => {
          toast({ title: 'First', message: 'Oldest of the stack.' });
          toast({ title: 'Second', message: 'In the middle.' });
          toast({ title: 'Third', message: 'Newest in front.' });
        }}
      >
        Push three
      </PixelButton>
    </TriggerRow>
  );
}

export function Stacked() {
  return (
    <PxlKitToastProvider stacked stackVisible={2}>
      <StackedTriggers />
    </PxlKitToastProvider>
  );
}

Flat

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function StackedTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() => {
          toast({ title: 'First', message: 'Oldest of the stack.' });
          toast({ title: 'Second', message: 'In the middle.' });
          toast({ title: 'Third', message: 'Newest in front.' });
        }}
      >
        Push three
      </PixelButton>
    </TriggerRow>
  );
}

export function Flat() {
  return (
    <PxlKitToastProvider stacked={false}>
      <StackedTriggers />
    </PxlKitToastProvider>
  );
}

Pixel Surface

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function SurfaceTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() =>
          toast({ title: 'Pixel surface', message: 'HP-bar accent on the left edge.', tone: 'cyan' })
        }
      >
        Push pixel toast
      </PixelButton>
    </TriggerRow>
  );
}

export function PixelSurface() {
  return (
    <PxlKitToastProvider surface="pixel">
      <SurfaceTriggers />
    </PxlKitToastProvider>
  );
}

Linear Surface

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function SurfaceTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() =>
          toast({ title: 'Pixel surface', message: 'HP-bar accent on the left edge.', tone: 'cyan' })
        }
      >
        Push pixel toast
      </PixelButton>
    </TriggerRow>
  );
}

export function LinearSurface() {
  return (
    <PxlKitToastProvider surface="linear">
      <SurfaceTriggers />
    </PxlKitToastProvider>
  );
}

Loading → Success

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function LoadingTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() => {
          const id = toast.loading('Uploading…', 'Hang tight.');
          setTimeout(() => {
            toast.update(id, {
              title: 'Uploaded',
              message: 'File is ready.',
              tone: 'green',
              loading: false,
              duration: 4500,
            });
          }, 1000);
        }}
      >
        Run loading → success
      </PixelButton>
    </TriggerRow>
  );
}

export function Loading() {
  return (
    <PxlKitToastProvider>
      <LoadingTriggers />
    </PxlKitToastProvider>
  );
}

Promise Flow

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function PromiseTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() =>
          toast.promise(
            () => new Promise<string>((resolve) => setTimeout(() => resolve('ok'), 1000)),
            {
              loading: { title: 'Saving…' },
              success: { title: 'Saved', message: 'All set.' },
              error: { title: 'Failed', message: 'Try again.' },
            },
          )
        }
      >
        Run promise
      </PixelButton>
    </TriggerRow>
  );
}

export function PromiseFlow() {
  return (
    <PxlKitToastProvider>
      <PromiseTriggers />
    </PxlKitToastProvider>
  );
}

Promise Rejected

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function RejectedPromiseTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        tone="red"
        onClick={() =>
          toast
            .promise(
              () => new Promise<string>((_resolve, reject) => setTimeout(() => reject(new Error('Network down')), 1000)),
              {
                loading: { title: 'Saving…' },
                success: { title: 'Saved', message: 'All set.' },
                error: { title: 'Failed', message: 'Try again.' },
              },
            )
            // The error toast tells the user; the rejection needs no other handling.
            .catch(() => {})
        }
      >
        Run failing promise
      </PixelButton>
    </TriggerRow>
  );
}

export function PromiseRejected() {
  return (
    <PxlKitToastProvider>
      <RejectedPromiseTriggers />
    </PxlKitToastProvider>
  );
}

Max Limit

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function MaxTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        onClick={() => {
          for (let i = 1; i <= 5; i += 1) {
            toast({ title: `Toast ${i}`, message: 'Only the latest two stay.' });
          }
        }}
      >
        Push five (max 2)
      </PixelButton>
    </TriggerRow>
  );
}

export function MaxLimit() {
  return (
    <PxlKitToastProvider max={2}>
      <MaxTriggers />
    </PxlKitToastProvider>
  );
}

With Action

import React from 'react';
import { PxlKitToastProvider, useToast, PixelButton } from '@pxlkit/ui-kit';

function TriggerRow({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-wrap gap-2">{children}</div>;
}

function ActionTriggers() {
  const { toast } = useToast();
  return (
    <TriggerRow>
      <PixelButton
        size="sm"
        tone="red"
        onClick={() =>
          toast({
            tone: 'red',
            title: 'File deleted',
            message: 'You can still restore it.',
            action: (
              <PixelButton size="sm" tone="red" variant="outline">
                Undo
              </PixelButton>
            ),
          })
        }
      >
        Push with action
      </PixelButton>
    </TriggerRow>
  );
}

export function WithAction() {
  return (
    <PxlKitToastProvider>
      <ActionTriggers />
    </PxlKitToastProvider>
  );
}

Related