# New: Toast Component in Frosted UI
We've added a built-in toast notification system to Frosted UI, replacing our dependency on Sonner. This gives us full control over styling, animations, and behavior — all integrated with our design tokens.
## Why
- No more external dependency — Sonner required unstyled: true plus manual classNames to match our design system. Now toasts use Frosted UI tokens natively.
- Built on Base UI — Uses @base-ui/react/toast under the hood for accessibility (ARIA live regions, keyboard navigation, focus management) with our own styling and interaction layer on top.
- Sonner-compatible API — The imperative toast() API is nearly identical to Sonner's. Most call sites only need an import change.
## Setup
Drop <Toaster /> inside your <Theme> — it renders its own portals, no wrapper needed:
import { Toaster } from 'frosted-ui';
<Theme>
<App />
<Toaster position="bottom-right" timeout={5000} limit={3} />
</Theme>## API at a Glance
import { toast } from 'frosted-ui';
// Variants
toast('Notification received');
toast.success('Changes saved');
toast.error('Upload failed');
toast.warning('Session expires in 5 minutes');
toast.info('Camera access denied');
toast.loading('Connecting...');
// With description
toast.error('Withdrawal failed', {
description: 'Insufficient balance. Try a smaller amount.',
});
// With action button
const id = toast.success('Message archived', {
actionProps: {
children: 'Undo',
onClick: () => toast.dismiss(id),
},
});
// Promise pattern (loading → success/error)
toast.promise(saveChanges(), {
loading: 'Saving...',
success: 'Saved!',
error: 'Something went wrong',
});
// Loading → replace (manual control)
const id = toast.loading('Generating stream key...');
// later...
toast.success('Stream key ready', { id });
// Dismiss
toast.dismiss(id);
toast.dismissAll();
// Per-toast positioning
toast.success('Done', { position: 'top-center' });### Positions
bottom-right (default), bottom-left, bottom-center, top-right, top-left, top-center
Each position maintains its own independent stack.
## Custom Toasts
For anything beyond the standard layout, use toast.custom(). The render callback receives { close, id, Toast } with composable sub-components:
toast.custom(
({ close, Toast }) => (
<Toast.Root>
<Toast.Content>
<Toast.Title>Update available</Toast.Title>
<Toast.Description>v2.4.1 is ready.</Toast.Description>
<div style={{ display: 'flex', gap: 'var(--space-2)' }}>
<Button size="1" variant="soft" onClick={close}>Later</Button>
<Button size="1" variant="solid" onClick={() => { close(); restart(); }}>
Restart now
</Button>
</div>
</Toast.Content>
</Toast.Root>
),
{ duration: Infinity },
);Hooks work inside the callback — it's treated as a component. You can build file upload progress bars, incoming call notifications, multi-action prompts, or anything else.
## Deduplication
Pass an explicit id to prevent duplicate toasts. If a toast with that ID already exists, it updates in place with a subtle bounce animation:
toast.error('Network disconnected', { id: 'network-status' });
// Later, same ID resolves it:
toast.success('Network restored', { id: 'network-status' });## Telemetry / Sentry
The onToast callback on <Toaster> fires once per toast creation:
<Toaster onToast={(t) => {
if (t.type === 'error') {
Sentry.captureMessage(String(t.title), { level: 'error' });
}
}} />## Storybook
All patterns are documented in Storybook under Components / Toast with interactive examples for every variant, promise patterns, custom content, deduplication, positioning, and more.




