RN-universal Toaster pilot
ScaffoldedStacked, screen-edge anchored notifications. Imperative toast() (sonner-parity) plus a declarative <RnToaster toasts={...} /> surface for A2UI agents. Built pure RN — no library.
Side-by-side: basic toast
Toast types
The icon and aria-live region carry the type;error uses aria-live="assertive", everything else uses polite.
With action
Promise toast
Custom duration
Declarative mode (A2UI agent surface)
The toaster on the top-right is fully controlled by a useState array — the imperative toast() queue is bypassed for that instance. An agent would own the array and dispatch dismissals through onDismiss.
Agent state: []
Note: declarative mode currently dismisses on the parent's schedule; the timer-driven auto-dismiss only runs in imperative mode. Pass duration through your own state machine.
Library decision
Per Phase 9 Q2, every Tier B primitive must record its library choice. RnToaster ships with no new runtime dependency. Full kickoff doc: docs/PHASE_9D_SONNER_KICKOFF.md.
- sonner (MIT, ~3.6 M/wk) — evaluated and rejected. Web-only; uses DOM portals, document-level event listeners, and focus management. Same
.web/.nativesplit blocker as cmdk and downshift. - react-native-toast-message (MIT, ~473 k/wk) — evaluated and rejected. RN-Web works, but the API (
Toast.show()with positional args) does not match sonner. Wrapping it to match sonner is most of the work; building the queue from scratch is cleaner. - burnt (MIT, ~128 k/wk) — evaluated and rejected. iOS / Android only via Expo Modules; fails the universal scope.
- This implementation — pure React Native (
View+Animated+ a module-level event-emitter store). ~280 LOC. Same code path on web and native. Zero added bundle weight.
Semantics
| Aspect | shadcn (sonner) | RN | Notes |
|---|---|---|---|
| Imperative API | toast(), toast.success(), … | toast(), toast.success(), … | Sonner-parity surface |
| Declarative API | — (queue is internal) | controlled toasts prop | A2UI / agent path |
| Promise | toast.promise() | toast.promise() | Same shape |
| Position | 6 presets | 6 presets | top-right, top, top-left, bottom-right, bottom, bottom-left |
| Stack cap | visibleToasts | visibleToasts (default 3) | Older toasts dismiss when limit hit |
| Animations | CSS transitions | Animated.timing | Slide + fade; ~180 ms |
| Theming | CSS variables via next-themes | tokens via .dark / brand class | Both inherit from the surrounding theme |
| aria-live | polite/assertive per type | polite/assertive per type | Error is assertive; everything else polite |
| Swipe-dismiss | yes | no (v1) | 0.2.x candidate |
| toast.custom(jsx) | yes | no (v1) | Declarative description: ReactNode covers most cases |