structyl
@structyl/styled

Toast

Imperative, globally-scoped toast notifications. Drop <Toaster /> once in your root layout and call toast.* from anywhere — event handlers, async functions, or outside React.

bash
pnpm add @structyl/styled

Setup

Add <Toaster /> once anywhere in your component tree — typically the root layout. Toasts fired anywhere in your app will render there.

tsx
// app/layout.tsx
import { Toaster } from '@structyl/styled';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

Variants

Six built-in variants — try them all live below.

tsx
import { toast } from '@structyl/styled';

toast.success('Changes saved', { description: 'Your profile has been updated.' });
toast.error('Upload failed', { retry: () => upload() });
toast.warning('Storage almost full');
toast.info('Update available');
toast.loading('Processing…');                // duration: Infinity by default
toast.show({ title: 'Custom', variant: 'default' }); // full control

Promise toast

toast.promise shows a loading state while the promise is pending, then automatically transitions to success or error.

tsx
toast.promise(
  fetch('/api/save').then(r => r.json()),
  {
    loading: 'Saving…',
    success: (data) => `Saved ${data.name}!`,
    error:   (err)  => `Error: ${err.message}`,
  },
);

Placement

Set the default position on <Toaster />, or override per-toast via horizontal / vertical options.

tsx
// Default for all toasts — bottom right
<Toaster horizontal="right" vertical="bottom" />

// Override for a specific toast
toast.error('Auth expired', { horizontal: 'center', vertical: 'top' });

Update & dedup

Passing the same id to any toast.* call replaces the existing toast rather than stacking a new one.

tsx
const id = toast.loading('Uploading file…');

// Later — replaces the loading toast in-place
toast.success('Upload complete!', { id, description: 'Your file is ready.' });
toast.error('Upload failed',      { id, retry: () => upload() });

Dismiss & remove

tsx
const id = toast.info('You have 3 unread messages');

toast.dismiss(id);   // plays the exit animation
toast.remove(id);    // instant removal, no animation
toast.dismiss();     // dismiss ALL toasts

Toaster props

PropTypeDefaultDescription
horizontal'left' | 'center' | 'right''right'Default horizontal alignment for toasts.
vertical'top' | 'bottom''bottom'Default vertical alignment for toasts.
maxToastsnumber5Maximum number of toasts visible at once per position group.
classNamestringExtra classes forwarded to the viewport wrapper.

Toast options

Passed as the second argument to any toast.* method.

OptionTypeDefaultDescription
idstringautoReuse to update an existing toast instead of stacking a new one.
descriptionstringSecondary text shown below the title.
variantToastVariant'default'One of: default | success | error | warning | info | loading.
durationnumber4000Auto-dismiss delay in ms. Pass Infinity to keep until manually dismissed.
horizontal'left' | 'center' | 'right'Toaster defaultOverride horizontal placement for this toast.
vertical'top' | 'bottom'Toaster defaultOverride vertical placement for this toast.
retry() => voidAdds a Retry button that calls this function when clicked.
action{ label: string; onClick: () => void }Custom action button. Takes priority over retry for the button label.
onDismiss(id: string) => voidCalled right before the toast is dismissed.

useToast

Subscribe to the toast store from inside a React component. Useful for building a custom Toaster or reading the current toast list.

tsx
import { useToast } from '@structyl/styled';

function CustomToaster() {
  const { toasts, dismiss, remove } = useToast();

  return (
    <div className="fixed bottom-4 right-4 flex flex-col gap-2">
      {toasts
        .filter(t => t.open)
        .map(t => (
          <div key={t.id} className="rounded-xl bg-card border border-border px-4 py-3 shadow-lg">
            <p className="font-semibold">{t.title}</p>
            {t.description && <p className="text-sm text-muted-foreground">{t.description}</p>}
            <button onClick={() => dismiss(t.id)}>Close</button>
          </div>
        ))
      }
    </div>
  );
}

Accessibility

Toast is built on the Radix Toast primitive which handles ARIA live regions automatically:

  • • The viewport has role="region" and aria-label="Notifications"
  • • Each toast is announced to screen readers as a live region update
  • • Focus is not moved to the toast — users can continue their current task
  • • The hotkey F8 moves focus to the toast viewport for keyboard users
  • • Toasts can be closed with Escape or swiped on touch devices