Phase 9d · Tier B

RN-universal Toaster pilot

Scaffolded

Stacked, 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

shadcn (sonner)
RN (anchored bottom-right)

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/.native split 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

Aspectshadcn (sonner)RNNotes
Imperative APItoast(), toast.success(), …toast(), toast.success(), …Sonner-parity surface
Declarative API— (queue is internal)controlled toasts propA2UI / agent path
Promisetoast.promise()toast.promise()Same shape
Position6 presets6 presetstop-right, top, top-left, bottom-right, bottom, bottom-left
Stack capvisibleToastsvisibleToasts (default 3)Older toasts dismiss when limit hit
AnimationsCSS transitionsAnimated.timingSlide + fade; ~180 ms
ThemingCSS variables via next-themestokens via .dark / brand classBoth inherit from the surrounding theme
aria-livepolite/assertive per typepolite/assertive per typeError is assertive; everything else polite
Swipe-dismissyesno (v1)0.2.x candidate
toast.custom(jsx)yesno (v1)Declarative description: ReactNode covers most cases