CloseWatcher API for Custom UI Layers
Model close as a lifecycle from device request through cancellation, commit, cleanup, and focus return without pretending CloseWatcher supplies dialog semantics.
CloseWatcher API lets a custom picker, sheet, or sidebar receive the browser's device-specific close request instead of guessing that every device means Escape. This tutorial builds one-layer-at-a-time dismissal with cancelable dirty-state review, reliable cleanup, focus restoration, and an explicitly degraded fallback.
CloseWatcher API starts after the native-element decision
Use a native <dialog> or popover when it already supplies the interaction semantics you need. A CloseWatcher API integration belongs to the remaining genuinely custom surface: perhaps a canvas inspector, bespoke picker, or transient application layer that cannot adopt the native container without losing required behavior.
The API provides a browser-mediated close-request channel. It can translate platform conventions into cancel and close events and participate in the browser's close-watcher stack. It does not give the layer a role or accessible name, trap focus, make the background inert, restore focus, draw a close button, or decide what unsaved work means.
That boundary is the architecture. Browser policy chooses whether a device close request reaches a watcher. Product policy decides whether dirty work may close. Component code owns semantic state, cleanup, and focus. The WHATWG HTML close-request algorithms define the stack, groups, activation constraints, cancelability, and event order.
First make the native-element decision with popover vs dialog. If the result is custom, write a lifecycle receipt before adding listeners. The CloseWatcher API is useful precisely because its limited ownership is explicit.
- Keyboard Escape is one possible device close input.
- A supported mobile back action is another; it is not a keyboard event.
- The browser applies close-watcher group and activation policy.
- Only the top eligible custom component receives the semantic request; the application owns its next state.
Map device close requests to one semantic intent
Escape on a desktop keyboard and a back gesture or button on a supported mobile device can mean “dismiss the top closeable thing.” They are not the same input event. A global keydown listener sees only keyboard traffic and guesses which layer owns it; it cannot establish a device-independent close channel.
CloseWatcher lets the browser interpret the device signal and target an eligible watcher. The application responds to a semantic close request instead of translating every hardware convention itself. A visible close button should call the same product transition so mouse, touch, keyboard, switch, and device requests converge on one policy.
Do not intercept browser history to simulate mobile dismissal. history.pushState() and popstate create navigation state, surprise the Back button, and still do not prove platform close behavior. An Escape-only fallback is acceptable when it says exactly what it is: “Escape + visible button only; device back not intercepted.”
The current MDN CloseWatcher reference describes the interface and device-specific purpose and, checked 2026-10-03, labels it “Limited availability.” Capability detection therefore belongs in both the UI and receipt. A CloseWatcher API path and its fallback are two named modes, not an invisible polyfill claim.
Keep cancel and close as different transitions
A close request begins policy review. When the cancel event is cancelable, dirty-state code may call preventDefault() and keep the layer open. The component can show a confirmation, save, or discard. A later confirmed attempt may request closing again. When the event is not cancelable, the application cannot block it and must proceed to cleanup.
The close event is the commit signal. Remove the layer's visible and semantic state, destroy or release its watcher, then restore focus. Calling close() requests the commit path directly; it does not fire cancel. Calling destroy() disables the watcher and emits neither event. Keep those methods distinct in tests.
Consider a dirty picker. Event 1 is request-close(cancelable:true); event 2 is cancel; event 3 is prevent; the layer stays open and confirmation becomes pending. Event 4 is confirm; event 5 is the retrying request-close; event 6 is its accepted cancel review; event 7 is close; event 8 destroys the watcher; event 9 removes the layer; event 10 returns focus. A non-cancelable request skips prevention even if the model is dirty.
This transcript is the heart of the CloseWatcher API receipt. It prevents a common mutation where “close requested” is treated as “closed,” erasing unsaved-state review and making cleanup order impossible to verify.
Let the browser choose the active watcher group
The browser maintains close-watcher groups and selects from the last eligible group, generally in reverse creation order. User activation influences grouping and the number of watchers that can be established. Those constraints help prevent a page from banking unlimited barriers against the user's attempt to leave or dismiss UI.
Application state can mirror visible layers for rendering, but it should not invent a parallel global stack that claims to replace browser policy. Create a watcher when the corresponding layer opens through the permitted interaction. On a close request, update only that owning layer. A request to the top picker must not also close the sheet underneath.
The pure simulator in the lab deliberately processes one top eligible layer. Its stack is a product model used to audit state transitions, not a browser conformance model. The optional native panel creates only one real watcher after a user action and reports observed events separately. It never calls a programmatic method and labels that evidence as an Android back test.
For a complete component implementation, compare the state model with the accessible drawer implementation. The CloseWatcher API adds a close-request channel; it does not replace the drawer's own semantics, focus behavior, or visible controls.
| Action | State before | Event order | State after |
|---|---|---|---|
| requestClose, clean | open | cancel then close when allowed | closed; watcher removed |
| requestClose, dirty/cancelable | open | cancel; prevent | open; confirmation pending |
| confirmed retry | confirmation pending | confirm; request-close; cancel; close | closed; watcher removed |
| requestClose, non-cancelable | open | close cannot be prevented | closed; watcher removed |
| close() | open | close only | closed; watcher removed |
| destroy() | any live watcher | no cancel or close event | watcher disabled |
Reading rule: labels, patterns, markers, and the semantic content carry every conclusion; color is supplementary.
Destroy watchers with the component lifecycle
A watcher must not outlive the layer it represents. Destroy it after close, on unmount, when a route replacement removes the component, or when a new instance supersedes it. If construction uses an AbortSignal, aborting that owner is another explicit cleanup path. Record which one fired.
Stale callbacks are more than a memory problem. They can close a newly mounted surface, move focus to a disconnected node, or produce analytics for an interface the user no longer sees. The leak count in the receipt is therefore live watchers - live closeable layers, with zero as the required final state.
Make cleanup idempotent. A second destroy should be a recorded no-op, not an exception or duplicated close. An unmount-before-request fixture should remove the watcher so the later request finds no eligible layer. Replacement should destroy the old owner before activating the next one.
Keep listeners and watcher references inside the component lifecycle rather than a module singleton. A singleton makes tests deceptively easy while coupling routes and independent surfaces. CloseWatcher API integration is safest when ownership follows the DOM and application state that can actually disappear.
Restore semantics and focus explicitly
A custom layer needs an appropriate role, accessible name, keyboard order, visible close affordance, and modality behavior. If it is truly modal, background inertness and focus containment belong to the application or a native dialog. The WAI-ARIA modal dialog pattern describes initial focus, Escape behavior, naming, modal containment, and return-focus expectations.
On committed close, focus should usually return to the connected invoker. If that element was removed, choose a named logical fallback that still exists, such as the control that replaced it or the next workflow step. If neither target is connected, leave focus unchanged and record no-valid-target; do not aim at a detached node or silently force the document body.
Focus return comes after visual and semantic removal so assistive technology does not land behind an active modal surface. The receipt should name the attempted invoker, whether it was connected, the fallback, final target, and reason. Browser focus foundations cover the broader navigation model.
The CloseWatcher API neither knows nor repairs these relationships. Keeping a responsibility matrix beside the lifecycle prevents its convenient events from being mistaken for an accessible component.
| Responsibility | CloseWatcher | Dialog | Popover | Application |
|---|---|---|---|---|
| Device close channel | browser-mediated channel | integrated close behavior | light-dismiss behavior where applicable | does not synthesize device back |
| Active top-layer/group policy | browser watcher group | browser top layer | browser top layer | must not shadow browser policy |
| Cancelable unsaved-work review | cancel event channel | cancel event for close request | not an equivalent dirty-state contract | product policy |
| Role | none | dialog semantics from the element | no dialog role implied | required for a custom layer |
| Accessible name | none | author supplies it | author supplies it when needed | author responsibility |
| Modal inertness | none | showModal supplies modal behavior | popover is non-modal | required for a custom modal |
| Focus placement and return | none | verify for the dialog workflow | verify for the popover workflow | restore only to a connected target |
| Visible close UI, fallback, analytics | none | author supplies and verifies | author supplies and verifies | owns explicit fallback limitations |
Reading rule: labels, patterns, markers, and the semantic content carry every conclusion; color is supplementary.
Ship an honest progressive fallback
Detect with "CloseWatcher" in window at the moment the custom layer opens. When supported, create the watcher inside the user-initiated path and attach cancel/close handlers before exposing the layer. When unsupported, retain the visible close button and a scoped Escape listener while the layer is mounted.
Label the fallback exactly: Escape and visible button only; device back is not intercepted. Do not synthesize a keyboard event and report it as browser behavior. Do not add a history entry to trap Back. Do not hide capability status from support or analytics; it is necessary context when a device report arrives.
Often the best fallback is architectural: use native dialog or popover instead of preserving a custom surface everywhere. Declarative controls such as HTML commandfor may further reduce application-owned event wiring where supported. Progressive enhancement means the core task remains possible, not that every device signal has been impersonated.
Track capability mode, layer kind, request source when knowable, cancellation result, cleanup result, and focus outcome without collecting raw user content. Record Escape key handling and custom UI dismissal as fallback capabilities, not native device proofs. A CloseWatcher API rollout succeeds when it removes device-specific guessing while unsupported users still have an obvious, operable dismissal route.
Publish the close-state receipt
The release checklist is compact: justify why native dialog or popover was not used; create at most one watcher per live custom layer; distinguish request, cancel, close, and destroy; honor cancelability; process only the active owner; destroy on close and unmount; remove semantic state; restore focus to a connected target; and display the fallback limitation.
Replay clean close, dirty prevention and confirmation, non-cancelable close, two-layer top-only behavior, unmount-before-request, double destroy, detached invoker, valid fallback, and no-target cases. Keep the pure state-machine hash separate from native capability observations so browser identity cannot make the canonical artifact nondeterministic.
The resulting CloseWatcher API design is not a clever Escape handler. It is a small state machine whose browser request channel, product decision, semantic cleanup, and focus return remain independently testable. That separation also makes future support changes easier: the browser adapter may improve without rewriting the component's close policy or its accessible contract.
Runnable local artifact — CloseWatcher supplies a browser-mediated request channel, not roles, names, focus trapping, inertness, focus restoration, or proof of a physical Android back gesture.
Validate up to eight layers and 64 fixed-catalog events, process only the top eligible layer, clean watchers on close or unmount, and export a deterministic transition receipt.