Skip to content

RFD: Toast notifications without app-owned visibility state #39

Description

@softmarshmallow

Status

Focused RFD discussion. The problem is open for study; the design is not
selected. No implementation is authorized or planned by this issue. This
does not create a numbered Uhura RFC, reserve the name toast, or commit Uhura
to a built-in API.

“Toast” here means an in-app transient notification, also commonly called a
snackbar or a Sonner-style toast. It does not mean an operating-system
notification or Android's system Toast.

Problem

Many applications need brief, non-blocking feedback such as “Saved”, “Upload
failed”, or “Undo delete”. In common toast systems, application code requests a
notification and moves on:

toast("Saved")

The spelling is illustrative only. The important property is that the
application does not add state solely to manage open, schedule a timeout, or
remove the notification after an exit animation.

This is not actually stateless. A long-lived notification host owns transient
instances, stable identity, ordering, timeouts, pause/resume, updates,
dismissal, announcements, and enter/exit presentation. That ownership is what
makes the authoring API feel fire-and-forget.

Uhura users can compose the visible card, but faithfully rebuilding that
lifecycle in every application is both difficult and easy to get wrong.
Requiring every app to model a queue and timer in machine state would also make
a presentation convenience look like domain state.

Current Uhura boundary

The widget taxonomy
distinguishes Element, Surface, and Pattern as primary forms, with Integration
and Behavior as cross-cutting facets. It explicitly permits first-party
capability without assuming that capability is a base-language Element.

The current 0.4 UI body is a pure projection. It cannot create callbacks,
promises, subscriptions, lifecycle effects, or browser authority
(application profile).
Pure components also create no runtime instance, state, lifecycle, context, or
scheduler; physical animation frames remain renderer state unless promoted
through a named semantic contract
(component boundary).
The deterministic kernel has no ambient clock; external work begins only after
a committed command, and later results return as declared inputs
(kernel contract).

The Instagram example demonstrates the current cost. Its notice banner needs a
machine-owned Option<Text> plus show, dismiss, and DismissNotice,
a pure NoticeBar,
and explicit conditional mounting and event wiring.
That is an honest inline banner, not a timed toast, and adding queueing,
replacement, or expiration would multiply the application-owned machinery.

Uhura already has a send-only SinkPort<T>,
which is relevant precedent for a fire-and-forget request. It does not provide
a notification host, visual adapter, queue, timing, or accessibility contract.

Those are useful constraints:

  • domain-significant notice state should remain explicit machine state;
  • a renderer must not smuggle semantic time or hidden machine transitions into
    a visual effect;
  • a pure UI declaration is not a place to run an imperative toast library; and
  • the absence of a general component/lifecycle facility is not by itself proof
    that Toast belongs in the intrinsic element catalogue.

The open question is whether transient notification lifetime can be owned
locally by a checked renderer/host capability, while any meaningful action or
result returns through an ordinary checked machine input.

Prior art

The useful precedent is not one visual style. It is the repeated split between
a request, a persistent host, and a host-owned lifecycle.

System Ownership model Relevant behavior
Sonner One long-lived Toaster; toast() may be called independently of the rendered item tree Calls return IDs; an ID can update or dismiss a toast; loading/promise states update in place; duration may be finite or persistent; the host limits the visible stack. Its promise helper is precedent for keyed state changes, not a request to add JavaScript promises to Uhura.
Radix Toast Provider/viewport plus individually rendered controlled or uncontrolled roots Shared duration, pause on hover/focus/window blur, swipe dismissal, foreground/background announcement policy, and animation hooks. Radix remains low-level: authors still build an imperative event-to-toast layer when they need one.
React Aria Toast and React Spectrum Toast A toast region backed by component state or a global queue Stable keys, maximum visible count, overflow queue, close/pause/resume/clear, keyboard access, focus restoration, minimum timeout guidance, and no automatic timeout for actionable toasts.
Compose SnackbarHostState A remembered host owns the queue and current snackbar showSnackbar is command-like, shows at most one item, queues concurrent calls, and completes with action-performed or dismissed. Duration may be short, long, or indefinite.

Accessibility standards reinforce that this is more than a timer:

  • WCAG status messages must be
    programmatically available without moving focus.
  • status and alert have
    different announcement urgency; ordinary updates should not all become
    assertive interruptions.
  • Timing Adjustable
    uses a five-second toast as an example: automatic removal is safe only when
    the information remains available elsewhere, or the limit can be disabled,
    adjusted, or extended.
  • prefers-reduced-motion
    separates a user's motion preference from the semantic lifetime of the
    message.

Working taxonomy hypothesis

Toast probably is not one new catalog Element. At minimum it contains three
separable contracts:

  1. Request: a checked, command-like application affordance that asks to show,
    update, or dismiss a transient notification.
  2. Host/region: a scoped or application-wide owner of the queue, visible
    instances, placement, and accessibility region.
  3. Lifecycle Behavior: identity, ordering, deadlines, pause/resume,
    dismissal reasons, announcement priority, and the boundary between logical
    removal and exit presentation.

The visual item may then be a maintained Pattern or kit skin over that lower
contract. This would allow a first-party convenience such as toast("Saved")
without making one themed toast card part of language law.

Time and animation are not interchangeable primitives:

  • a deadline determines when a presentation-only message is eligible for
    dismissal;
  • animation presents a lifecycle transition and must not decide semantic
    removal; and
  • a general machine clock is needed only if elapsed time changes machine
    meaning. A renderer-local timeout should stay outside the deterministic core
    when its only consequence is removing transient presentation.

A plausible lower capability is therefore not merely “timer” or “animation”.
It may be a scoped transient-host/queue contract with renderer-owned deadlines,
presence, announcement, and motion policy. This is a hypothesis for the RFD to
test, not a decision.

Design space

This RFD should compare at least:

  1. First-party Toast affordance. Uhura supplies the request API and a
    standard unstyled host/lifecycle. Applications configure content, variant,
    duration, placement, and optional checked actions.
  2. Headless transient-notification toolkit. Uhura supplies a queue/host API,
    stable identity, timing, announcements, and conformance rules; an
    application or kit supplies all visual composition.
  3. Lower general primitives plus a first-party package. A reusable
    transient-host/presence or host-timer capability is public, and Toast is the
    maintained reference composition over it.
  4. Explicit machine state only. Applications own every notification and
    timeout. This remains the baseline to compare, but must explain how ordinary
    users avoid repeatedly rebuilding queueing, accessibility, and renderer
    lifecycle.

The first-party affordance and public lower toolkit are not mutually exclusive.

Lifecycle questions

A candidate should define an operational lifecycle at least as precise as:

queued -> visible <-> paused -> dismissing -> removed
              | update          | action / close / timeout

It should then answer:

  1. Where may a request originate: a machine transition, a checked application
    command, a UI-event mapping, a provider result, or more than one of these?
  2. Is fire-and-forget the only form, or may a request return a stable key or a
    typed eventual result?
  3. Can a key update a visible or queued notification, including
    loading-to-success/error transitions? How are duplicate keys handled?
  4. Is the host global, per application entry, per route, per presentation, or
    explicitly scoped? What happens on navigation, surface closure, hot reload,
    and deployment replacement?
  5. Are notifications stacked or shown one at a time? What are ordering,
    overflow, deduplication, replacement, and maximum-visible policies?
  6. Which states/variants are semantic—neutral, success, warning, error,
    loading—and which are merely kit styling?
  7. How are default, custom, indefinite, and accessibility-adjusted durations
    represented? Which interactions pause a deadline?
  8. What are the dismissal reasons? Is timeout observable by the machine, or
    intentionally renderer-local?
  9. How does an action produce one checked machine input without callbacks or
    hidden re-entry? Must actionable notifications remain until explicitly
    handled or dismissed?
  10. When is a message announced politely as status, assertively as alert, or
    not at all? How are rapid updates and duplicate announcements prevented?
  11. How does keyboard access reach a notification action without stealing
    focus when the notification appears, and how is focus restored after
    removal?
  12. What information must remain discoverable outside a timed notification?
    What minimum timing, pause, dismissal, and reduced-motion guarantees are
    mandatory?
  13. Does logical dismissal remove the notification immediately from the
    semantic/accessible model while allowing renderer-owned exit motion, or is
    another ordering required?
  14. What is the deterministic Editor representation? Static examples should be
    able to pose a queue and lifecycle state without running a wall clock.
  15. What appears in traces and replay: show/update/dismiss requests, host
    expiration inputs, neither, or a boundary selected by semantic relevance?
  16. What does a renderer without overlays, live regions, hover, or animation
    support do? What is the immutable-export behavior?
  17. Which subset is general enough to support related transient UI without
    turning Uhura into a generic scheduler or animation engine?

Evidence required before any implementation

A serious proposal should include:

  • a small formal/operational model for queue, identity, pause, update, action,
    dismissal, and disposal;
  • ordinary save feedback, repeated errors, loading-to-result update, undo
    action, navigation, and hot-reload examples;
  • an explicit split between machine state, renderer-local state, and
    Editor-only posed state;
  • static Editor examples that never depend on elapsed wall time;
  • accessibility cases for polite/urgent announcement, action focus, timeout,
    persistent alternatives, rapid messages, and reduced motion;
  • at least a Web realization and one non-Web renderer comparison;
  • positive and negative checker cases for any checked request/action contract;
    and
  • conformance cases proving that exit animation cannot invent or delay a
    machine transition.

Exit criterion

This RFD is ready to close when it records:

  • Toast's taxonomy and owner boundaries;
  • whether Uhura provides a first-party affordance, a headless toolkit, lower
    primitives, or a layered combination;
  • the request/update/dismiss/action contract without committing to raw
    callbacks or promises;
  • queue, lifetime, accessibility, Editor, replay, and renderer-fallback
    semantics;
  • whether any general time/presence/motion capability is actually required;
    and
  • whether the conclusion is small and durable enough to prepare as a numbered
    Uhura RFC.

Non-goals

  • implementing Toast in this issue;
  • selecting a theme, card anatomy, icons, colors, or exact screen placement;
  • designing operating-system notifications, permission prompts, blocking
    alerts, or modal dialogs;
  • adding ambient time, JavaScript promises, or callbacks to the deterministic
    core;
  • solving the complete component, Surface, animation, or general scheduler
    design; or
  • accepting every related transient UI pattern by analogy.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    uhura/uiUhura UI language, widget taxonomy, and renderer contracts

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions