Component

Toaster

Sonner notifications in the brand’s dress.

betav0.3.0 Chromium 103 ui-scale safe

Install

Copies the file into components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.
bunx shadcn@3 add @fastrp/toaster

Examples

Default

Open alone

API

<Toaster />Takes the props of what it wraps

Sonner, dressed in the brand: square, a band ground, the status in a left rule. Pass `theme` from the app's own theme state — the component does not read one, so it runs in the game, which has no theme provider. Mount once; call `toast()` anywhere.

Dependencies

npm

  • sonner@^2.0.7

Source

components/ds/toaster.tsxShow
'use client';

import * as React from 'react';
import { Toaster as Sonner, toast, useSonner } from 'sonner';
import { type UiSound, useUiSound } from '@/components/ds/ui-sounds';

// @fast-ds [email protected]

export type ToasterProps = React.ComponentProps<typeof Sonner>;

/**
 * Sonner, dressed in the brand: square, a band ground, the status in a left rule. Pass `theme`
 * from the app's own theme state — the component does not read one, so it runs in the game, which
 * has no theme provider. Mount once; call `toast()` anywhere.
 */
function Toaster({ toastOptions, ...props }: ToasterProps) {
  useToastSounds();
  return (
    <Sonner
      className="toaster group"
      toastOptions={{
        ...toastOptions,
        classNames: {
          toast:
            'group toast !rounded-lg border !border-foreground/10 !bg-[hsl(var(--panel)/0.96)] !text-foreground !shadow-2xl data-[type=success]:border-l-2 data-[type=success]:!border-l-success data-[type=error]:border-l-2 data-[type=error]:!border-l-danger data-[type=warning]:border-l-2 data-[type=warning]:!border-l-warning data-[type=info]:border-l-2 data-[type=info]:!border-l-info',
          title: '!font-display !text-base !uppercase !tracking-[0.04em]',
          description: '!text-muted-foreground',
          actionButton:
            '!rounded-md !bg-foreground !font-display !uppercase !tracking-[0.04em] !text-background',
          cancelButton: '!rounded-md !bg-foreground/10 !text-foreground',
          icon: 'group-data-[type=success]:text-success-text group-data-[type=error]:text-danger-text group-data-[type=warning]:text-warning group-data-[type=info]:text-info',
          ...toastOptions?.classNames,
        },
      }}
      {...props}
    />
  );
}

const TOAST_SOUND: Partial<Record<string, UiSound>> = { success: 'success', error: 'error' };

/**
 * A toast arriving is the one sound nobody asked for: `success` and `error` by its type, `notify`
 * for the rest. A loading toast is silent until it turns into its outcome.
 */
function useToastSounds() {
  const { toasts } = useSonner();
  const sound = useUiSound();
  const heard = React.useRef(new Set<string>());

  React.useEffect(() => {
    const present = new Set<string>();
    for (const item of toasts) {
      const key = `${item.id}:${item.type ?? 'default'}`;
      present.add(key);
      if (heard.current.has(key) || item.delete || item.type === 'loading') continue;
      sound(TOAST_SOUND[item.type ?? ''] ?? 'notify');
    }
    heard.current = present;
  }, [toasts, sound]);
}

export { Toaster, toast };