Skip to content

RFD: Modal, dialog, and sheet Surfaces with renderer-owned transitions #41

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, change the active 0.4 <Surface>
contract, reserve the names Modal, Dialog, or Sheet, or select a
transition design.

Terms are separated deliberately:

  • dialog is a transitory task or information window and may be modal or
    non-modal;
  • modal means interaction with the relevant parent scope is blocked until
    the presentation ends;
  • sheet and full-screen cover describe presentation geometry and
    adaptation, not modality by themselves; and
  • alert dialog is a response-oriented semantic specialization, not a
    visually emphatic ordinary dialog.

The RFD owns the general Surface lifecycle, semantics, dismissal, focus, and
motion question. It does not silently promote the Instagram comments sheet into
the general design.

Problem

A modal or sheet normally enters and exits with a transition. Uhura applications
must still own the meaningful binary fact—whether a presentation is requested
and what domain data it contains—but they should not need extra machine states
such as opening and closing merely to keep pixels mounted long enough to
animate.

The Instagram example exposes the missing boundary. Its comments Surface exists
only while the machine projects it. When the machine accepts DismissComments,
the next projection omits the subtree and the browser renderer removes its DOM
node immediately. CSS cannot animate an element after it has been removed.

That is not just missing CSS. A complete modal needs two coordinated but
different lifecycles:

machine target:
  absent | presented(surface-key, semantic content and actions)

renderer lifecycle:
  absent -> entering -> presented -> exiting -> absent

The machine owns product truth, checked inputs, domain data, and any meaningful
result. The renderer owns physical presence, layering, focus mechanics, a
backdrop, enter/exit progress, reduced-motion adaptation, and the moment an
outgoing visual can be disposed.

A platform close request, outside press, or sheet gesture also cannot mutate the
application behind the machine. It needs a declared checked dismissal input; the
machine may accept or refuse it. Conversely, once semantic closure is accepted,
a retained outgoing visual must not remain focusable, actionable, or falsely
modal merely because decorative motion is still finishing.

The RFD must decide whether this split belongs entirely to Surface, depends on a
smaller reusable presence primitive, or should be exposed through another
first-party toolkit boundary.

Current Uhura boundary

The stable Surface taxonomy router
already requires versioned Surface entries to separate semantic lifecycle from
renderer-owned presentation, focus, and motion. It also says that no Surface is
documented there yet.

The active 0.4 contract is intentionally smaller:

  • the checker admits only children and a required
    key;
    Surface has
    no events or constraints;
  • Core lowers every Surface to a keyed physical
    dialog render node;
  • Play realizes it as a
    frame-contained dialog:
    the page and lower Surfaces become inert, focus enters and is contained in the
    top Surface, focus is restored on removal, and host-owned Play chrome remains
    usable;
  • it deliberately does not use the browser's document-wide modal top layer or
    claim document-wide aria-modal, because the surrounding Editor/Play shell
    is outside the application frame;
  • Escape and the scrim cannot invent a machine input, so authors must provide an
    explicit checked close control; and
  • static Editor previews use the same contained stack but are wholly inert.

The browser implementation therefore uses
<dialog open>, not showModal(),
implements its own
initial focus, Escape suppression, and focus loop,
and manually
inerts, stacks, and restores.
This is a contained host adaptation, not native modal-dialog semantics. It even
needs tabindex="-1" on the physical dialog as a focus fallback, while the
HTML standard says authors must not put tabindex on a dialog element. That
mismatch is evidence that the durable semantic contract and the current host
mechanism have not yet been reconciled.

Presentation is also hard-coded: every Surface receives one
72%-height bottom-sheet style,
and Play supplies one
frame-sized scrim.
There is no declared distinction among centered dialog, alert dialog, bottom
sheet, side sheet, non-modal dialog, or full-screen presentation.

Most importantly, reconciliation
removes any absent node immediately.
The Instagram
CommentsSheet
has a checked Dismissed action, and the page
conditionally mounts it.
Its seven deterministic examples cover content states, but none can express an
entering or exiting renderer pose. There is no retained outgoing node,
transition phase, interruption/completion rule, or reduced-motion policy.

This is a useful executable incubation contract. It is not yet a general
Modal/Dialog/Sheet design.

Prior art

The transferable precedent is the separation among semantic desired state,
platform dismissal, surface orchestration, and renderer-owned presence. It is
not one library's visual style.

System Relevant contract Evidence for Uhura
HTML <dialog> show() is non-modal; showModal() places a dialog in the document top layer, makes the rest of that document inert, manages focus, and later restores it. Current closedby policy distinguishes explicit-only, platform close request, and light dismiss. requestClose() offers a cancelable close request; close(), close events, and returnValue separate request, accepted closure, and result. A dialog is a privileged semantic/lifecycle primitive, not just a positioned container. A portable dismissal policy should describe platform close requests—not only the desktop Escape key—and must not be inferred from disappearance.
HTML/CSS transition model HTML semantically closes the dialog and restores focus, while CSS can defer physical removal from the top layer through discrete display and overlay transitions. @starting-style supplies an entry pose. The platform itself distinguishes semantic closure from retained exit pixels. Newer overlay support is limited, so motion must be progressive enhancement rather than a semantic dependency.
WAI-ARIA modal dialog pattern Focus enters the dialog, Tab is contained, Escape closes, focus normally returns to the invoker, the dialog is named, and a visible close/cancel control is strongly recommended. aria-modal="true" is valid only when outside content is genuinely unavailable. Accessible name, initial-focus policy, containment, restoration, and truthful modality belong in the conformance contract. Alert dialog needs its own semantics and safe initial-action policy.
shadcn/ui Dialog, Sheet, and Radix Dialog The public anatomy separates root, trigger, portal, overlay, content, title, description, and close. Sheet extends Dialog with edge presentation. Radix exposes open/closed state and suspends unmount for CSS exit animation; forceMount delegates physical presence to another animation owner. The useful shadcn mental model is a stable semantic façade plus composable anatomy and replaceable realization. The current implementations are React-specific and cannot be imported as Uhura's runtime, but their open-versus-presence split is directly relevant.
Base UI Dialog It exposes modal/non-modal behavior, pointer-dismissal policy, transition status, starting/ending style states, keepMounted, explicit unmount, and completion after animations. A single dismissible Boolean and a single open bit are not enough for host behavior, even when application state remains binary.
Flutter showDialog<T>, DialogRoute, and modal bottom sheet A renderer-owned route provides barrier semantics, closed-loop focus traversal, safe-area/adaptive behavior, entrance/exit transitions, and a typed result. Route completed resolves only after the exit transition and overlay removal. Bottom sheets add scrim, gesture, sizing, and animation policy without redefining application data. Accepted dismissal/result and physical disposal are distinct phases. Dialog and sheet can share orchestration while retaining different presentation and interaction policy.
SwiftUI presentation modifiers and Apple modality guidance A Boolean or optional-item binding describes desired presentation; the system owns presentation and resets the binding after interactive dismissal. onDismiss is separate. Sheets add renderer-owned detents and adaptation; interactive dismissal can be disabled independently. Apple treats modality as blocking behavior and sheet/full-screen/popover as presentation choices. Uhura should distinguish desired semantic presence, dismissal request, and renderer presentation. Geometry may adapt by renderer, while selected detent is promoted only when product logic truly needs it.
Jetpack Compose Dialog and ModalBottomSheet A dismiss request does not remove the dialog; application state must change. Bottom-sheet guidance waits for hide animation completion before removing the sheet. This is the same missing boundary shown by Instagram: request, accepted state change, and retained exit presence are separate.
Spectrum Web Components overlay A vanilla custom-element host uses native dialog/popover top-layer machinery, distinguishes modal/page/auto/manual modes, manages focus, and passes open state to visual children for transitions. Web Components prove that a non-React adapter can package this lifecycle. Uhura should copy the semantic and disposal discipline, not expose arbitrary element instances or JavaScript callbacks.

Reduced motion is also a first-class input. SwiftUI exposes
accessibilityReduceMotion;
Flutter exposes
disableAnimations;
and the Web exposes
prefers-reduced-motion.
The same semantic open/close behavior must remain correct when transitions are
shortened, cross-faded, or omitted.

Working taxonomy hypothesis

A modal is probably not one new styled catalog Element. The durable contract
appears to have at least four layers:

  1. Surface orchestration capability. Keyed placement outside the ordinary
    element tree; scope, ordering, nesting, modality, inertness, focus, scroll
    lock, dismissal routing, restoration, and disposal.
  2. Semantic dialog family. Ordinary dialog versus alert/confirmation,
    accessible name and description, action/result roles, and initial-focus
    policy.
  3. Renderer-owned presence Behavior. Entry, presented, and exit phases;
    outgoing-subtree retention; interruption/reversal; reduced-motion policy;
    and completion. This may be reusable by Toast, but presence alone does not
    provide modality, focus, results, or a stack.
  4. Presentation policy and first-party kit. Automatic/centered/edge sheet/
    full-screen hints, anatomy, tokens, and visual defaults that can adapt by
    renderer without becoming portable CSS law.

A plausible ownership split is:

machine/application
  desired keyed Surface + domain data + checked actions/results
       |
       | committed projection / checked dismissal input
       v
Surface host
  semantic stack + modality + focus + dismissal routing
       |
       v
renderer presence
  entering | presented | exiting | disposed

This is a hypothesis for the RFD to test, not a selected API.

The default rule should remain that animation completion cannot invent, block,
or reorder a machine transition. A did-dismiss or completion observation
belongs in the machine only if an application can demonstrate semantic need;
otherwise it is host cleanup. Likewise, drag progress, backdrop opacity,
measured size, and transition progress remain renderer-local unless a named
portable contract promotes one of them.

Dismissal and result questions

The proposal should answer:

  1. Which close sources are portable: explicit authored action, cancel,
    platform close request (Escape/Back/assistive gesture), backdrop/light
    dismiss, sheet drag, programmatic machine removal, navigation, and host or
    system teardown?
  2. Is dismissal policy expressed per source rather than as one Boolean? What
    are safe defaults for ordinary dialog, alert dialog, and sheet?
  3. What checked event is emitted for a host dismissal request, and what typed
    reason does it carry? What happens while the machine refuses or has not yet
    committed closure?
  4. Are action roles such as cancel, confirm, close, and destructive semantic
    enough for renderer ordering, emphasis, and initial focus? How do actions
    carry typed results without callbacks?
  5. Is accepted closure observable separately from physical removal? When, if
    ever, may completion be promoted to machine state?
  6. What happens on duplicate dismissal, reopening the same key during exit,
    replacing a key during entry, navigation, hot reload, and renderer failure?
  7. Does an exiting visual remain a frozen snapshot, a live but inert subtree, or
    a renderer-specific choice? It must not emit a second action.
  8. When closing could lose draft data, how does the machine request
    confirmation without the renderer guessing whether closure is safe?

Modality, focus, and accessibility questions

The proposal should answer:

  1. Is modality scoped to an application document, a Play frame, a route, a
    window, or an explicit Surface host? Which surrounding controls remain
    available?
  2. When may the Web renderer use native showModal()? A native modal is
    document-scoped; using it inside the current shared shell would incorrectly
    inert Editor/Play chrome. Would an isolated application document such as an
    iframe permit native semantics, while contained previews use an explicit
    scoped realization?
  3. What is the portable distinction among modal dialog, non-modal dialog,
    popover, and other overlays? Shared layering infrastructure must not erase
    their different interaction and accessibility contracts.
  4. How are accessible name and optional description checked? When is
    alertdialog required, and how is excessive description avoided for
    structured content?
  5. How is initial focus chosen for ordinary forms, long structured content,
    destructive confirmations, and simple acknowledgement? How can authors
    express intent without arbitrary DOM selectors?
  6. How are Tab containment, platform close, visible dismissal, background
    inertness, scroll lock, and focus restoration tested across pointer,
    keyboard, touch, and assistive technology?
  7. How are nested Surfaces ordered? Which lower Surface is inert, what is the
    return-focus chain, and may an alert appear above another modal?
  8. At accepted semantic closure, when are inertness released and focus restored
    relative to retained exit pixels? The outgoing visual must be removed from
    interaction and accessibility at the correct point.
  9. What is the truthful accessibility contract for frame-contained modality
    where host chrome remains operable and document-wide aria-modal would be
    false?
  10. What does a renderer lacking focus, overlays, or native modality do? Which
    semantics are required for admission, and when must it fall back to an
    inline or full-screen presentation?

Presence, transition, and adaptation questions

The proposal should answer:

  1. Is presence a general Behavior, a private Surface-host mechanism, or a small
    public primitive shared with RFD: Toast notifications without app-owned visibility state #39?
  2. Which phases exist: entering, presented, exiting, interrupted, and disposed?
    Is transition progress ever portable, or only an Editor pose?
  3. How does a renderer retain outgoing content after the next semantic
    projection omits it? What identity and previous content are retained, and
    who disposes resources?
  4. Can entry or exit reverse smoothly when state changes mid-transition? What
    happens when the same key returns with different content?
  5. Which properties are semantic hints—presentation style, edge, detents,
    dismissal gesture—and which are renderer styling such as duration, curve,
    distance, opacity, and spring?
  6. Are centered dialog, bottom/side sheet, and full-screen cover distinct
    declarations or adaptive presentations of one semantic request?
  7. When does a selected sheet detent become machine state rather than
    renderer-local pose?
  8. How do reduced-motion, no-animation, low-power, static, and unsupported
    renderers preserve identical close/result/focus semantics?
  9. May the Web adapter rely on native @starting-style, discrete
    display/overlay transitions, or the View Transitions API only as
    progressive enhancement? What is the fallback?
  10. Does logical closure immediately remove the outgoing Surface from the
    accessibility tree and hit testing while a purely visual ghost exits, or is
    another ordering more correct across renderers?

Editor, replay, and static rendering

Editor evidence must remain deterministic and must not run a wall clock merely
to display motion.

A candidate should define explicit Editor-only poses for:

  • entering, presented, and exiting;
  • fixed transition progress;
  • backdrop state and presentation style;
  • selected sheet detent or drag pose;
  • intended initial-focus target and modal stack; and
  • reduced-motion/no-motion output.

These poses must not become application machine state by accident. Immutable
export may render one declared pose but need not capture animation or video.
That boundary should stay consistent with #15's static-rendering scope.

Traces and replay should record semantic presentation and checked dismissal/
action inputs. Renderer frame timing, easing samples, and animation-completion
callbacks should remain absent unless this RFD demonstrates a semantic reason
to promote them.

Design space

This RFD should compare at least:

  1. First-party semantic Surface family. Uhura supplies checked Dialog and
    AlertDialog semantics over a standard Surface host; Sheet/full-screen are
    adaptive presentation policies and a maintained kit supplies anatomy and
    styling.
  2. Configured generic Surface plus first-party patterns. One orchestration
    primitive accepts a narrow semantic modality/dismissal/focus contract;
    maintained Dialog/Sheet patterns compose it.
  3. Lower presence and overlay behaviors plus a headless toolkit. Uhura
    exposes reusable presence/layer/focus capabilities and builds Dialog as the
    first-party reference package. This must prove applications cannot combine
    the pieces into inaccessible half-dialogs.
  4. HTML-first Web realization plus normalized cross-renderer contract. Use
    native <dialog> in an isolated application document, map the same
    normalized semantics to Flutter/Swift-like native presentations, and retain
    a contained preview realization for Editor. Browser APIs remain adapter
    details, not Uhura source syntax.
  5. Explicit machine state and immediate composition only. Applications own
    every close state and renderer removal is immediate. This remains the
    baseline, but it must explain how users obtain exit motion, platform close
    requests, focus, nesting, accessible naming, and renderer portability
    without rebuilding a modal runtime per application.

These options may be layered rather than mutually exclusive.

Evidence required before implementation

A serious proposal should include:

  • a small formal model for desired presentation, dismissal request, accepted
    semantic closure, renderer presence, focus/inertness, and disposal;
  • illustrative authoring for an ordinary modal form, destructive alert dialog,
    bottom sheet, non-modal dialog, nested confirmation, refused dismissal, and
    no-motion fallback;
  • typed action/result and dismissal-reason examples with positive and negative
    checker cases;
  • a Web spike comparing native showModal() inside an isolated application
    document with the current frame-contained realization;
  • at least one non-Web mapping or executable comparison against Flutter and
    SwiftUI semantics;
  • accessibility tests for naming, description, initial focus, Tab containment,
    platform close, visible close, focus return, background inertness, nested
    modality, zoom, and assistive-technology dismissal;
  • lifecycle tests for enter, exit, reversal, replacement, duplicate request,
    same-key reopen, teardown, navigation, and resource disposal;
  • conformance proving that a retained outgoing visual is inert, excluded from
    accessibility and checked events, and cannot delay or invent a machine
    transition;
  • reduced-motion and renderer-without-animation tests that preserve semantic
    behavior;
  • deterministic Editor examples for every admitted phase and presentation
    policy; and
  • immutable/static output and fallback tests for renderers without overlays,
    focus, gestures, or motion.

Exit criterion

This RFD is ready to close when it records:

  • the Surface/Dialog/AlertDialog/Sheet taxonomy and which parts are semantic,
    orchestration, Behavior, Pattern, or Integration;
  • the application, host, and renderer state-ownership split;
  • modality scope, stack, backdrop, focus, accessibility, dismissal, action,
    result, and restoration semantics;
  • the renderer presence lifecycle and exact relationship between accepted
    closure and physical disposal;
  • adaptive presentation and reduced-motion rules;
  • the Web native-versus-contained host strategy and cross-renderer fallback;
  • whether presence is reusable with Toast or remains Surface-private; and
  • whether the conclusion is small and durable enough to prepare as a numbered
    Uhura RFC.

Non-goals

  • implementing or changing Modal/Dialog/Sheet in this issue;
  • changing the Instagram example;
  • selecting final source syntax, names, anatomy, theme, dimensions, transition
    duration, curve, spring, or visual direction;
  • making one bottom-sheet style the meaning of every Surface;
  • importing React, Radix, Base UI, shadcn/ui, Flutter, SwiftUI, or a Web
    Component runtime as Uhura language law;
  • adding animation-completion callbacks, timers, DOM selectors, or ambient
    browser authority to the deterministic core;
  • designing a general animation engine, navigation stack, window manager, or
    every overlay pattern;
  • treating Popover, Tooltip, Menu, Toast, and Dialog as interchangeable because
    they share a layer; or
  • requiring motion for semantic correctness.

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