Examples of popups may include action menus, teaching UI, or the listbox portion of a <select>
control
For many such use cases, it is incumbent upon the author to handle the popup’s styling, positioning and z-index stacking, focus management, keyboard interactions, and accessibility semantics. Because no platform-native solutions exist to comfortably handle all these use cases, individual authors and framework developers must continuously re-write the same classes of controls. This results in duplicative work for the web development community, and inconsistent experiences for users of these web applications.
The web platform can be extended such that authors can get popup interactions and styling “for free”, but have enough flexibility to support their individual use cases.
Authors can efficiently create popups which:
box-shadow
or backdrop-filter
CSS properties)There are many different types of elements that are meant to be displayed in the top layer of a web application, but not all may be popups. We propose that all popups share “light dismiss” behaviors. Other elements which are aesthetically similar to popups, but do not light dismiss, may be better addressed with the <dialog>
element or other new purpose-built elements. Such examples include: alerts, toasts, custom tooltips, and miscellaneous, persistent popover UI.
We propose a new HTML element called popup
. This new element can be used for any transient UI that “pops up” over all other web app UI. A popup
may be its own standalone element (such as a teaching bubble) or it may be part of a larger composited component (such as a combobox).
popup
will include:
popup
:popup
attribute, applied to whichever elements should invoke a given popup (if applicable).open
attribute, applied to the popup
to express that it should be shown.show()
JavaScript method, for invoking the popup
.autofocus
attribute which enables moving focus to the popup
or to a descendent.delegatesfocus
attribute, for passing focus to descendants.anchor
attribute, which both relates the popup
to an activating element and can be used in a separately-proposed, CSS-based anchor positioning scheme.popup
:popup
attribute.open
attribute from popup
.hide()
method for hiding the popup
.popup
attributeSection titled Option%20A%3A%20the%20popup%20attributeOne use case for a popup
is a popup menu triggered by a menu button:
An author could produce this popup menu using the following markup:
<button id="menuButton" popup="menuPopup">Menu</button>
<popup id="menuPopup" role="menu" anchor="menuButton">
<!-- Markup for menuitems goes here -->
</popup>
The popup
attribute on the button
element (referred to later in this document as the “invoking element”) takes an IDREF pointing to the relevant popup. Invoking this button will likewise invoke the popup with the matching ID.
This popup
attribute will apply accessibility semantics equivalent to aria-haspopup="true"
and aria-controls="menuPopup"
on the button (as well as a “controlled by” reverse relationship mapping on the popup
itself). This attribute is valid only on a subset of interactive elements:
button
or input
in the button state (input type="button"
). Invoking one of these elements will show the relevant popup.input
in the text
, email
, phone
, or url
states. Setting focus in one of these elements will show the relevant popup. Note: we may need to explore means of suppressing this invocation on focus, for instances where the author instead wishes to show the popup
based on text-entry logic.open
attributeSection titled Option%20B%3A%20the%20open%20attributeSome popups, such as teaching UI, might be shown to the user upon initial “page load”. For popups which aren’t shown as the result of a user interaction or JavaScript-controlled logic, apply the open
attribute to show the popup
:
<popup open>
<p>
<strong>New!</strong>
I’m some sort of educational UI…
</p>
…
</popup>
show()
methodSection titled Option%20C%3A%20the%20show%28%29%20methodSuppose that an author instead wants to show their teaching UI upon some app-internal logic. Such an author could instead call show()
on the popup
from script:
if (upsellNewFeature) {
document.getElementById('newFeatureUI').show()
}
Until the author has used the popup
attribute, open
attribute, or show()
method, the popup
does not display (has a computed value of none
for its display
property). Showing a popup places the popup
into a browser-managed stack of top-layer elements that allow the popup
to produce a box in accordance with author-supplied styles. Initial styles from the user agent stylesheet consist of:
popup {
display: block;
position: fixed;
top: 0;
left: 0;
}
Only one “top-level” popup
may be displayed at a time. When a popup
is shown and placed on the stack, it will remove all popup
s from the stack until it encounters an “ancestral” popup
, or the list is empty. In this way, the user agent will ensure that only one popup
and its child popup
s are ever displayed at a time—even across uncoordinated code.
The following would be considered an “ancestral” popup
:
popup
ancestor of the new popup
’s invoking element (based on the popup
attribute)popup
ancestor of the new popup
’s anchoring element (based on the anchor
attribute)popup
ancestor of the new popup
itselfOther events also remove a popup
from the stack, including loss of focus, or hitting ESC
(often referred to as light dimiss behaviors). Interactions with other elements like dialog
, or other future types of popup-like elements, for example, showing a menu, must also remove the popup
s from the top-layer stack. Dismissing a popup
will also remove any child popup
s from the stack.
popup
s in the stack are laid out and rendered from the bottom of the stack to the top. Each popup
will paint atomically as its own stacking context.
Showing a popup
via options A (popup
attribute) or C (calling the show()
) method will also cause the open
attribute to be set on the popup
.
autofocus
logicSection titled autofocus%20logicBy default, focus remains on the current active element when the popup
is invoked. If this element is somehow destroyed, focus moves to a focusable ancestral element.
popup
is inherently focusable, but is not reachable by sequential keyboard navigation by default (equivalent to tabindex=-1
). Authors can explicitly move focus to the popup
by calling focus()
, or implicitly to the popup
or one of its descendants using the autofocus
attribute.
To move focus to the popup
itself when show
is called—without the need to explicitly call popupElement.focus()
—place the attribute directly on the popup
:
<popup autofocus>...</popup>
To move focus to a descendent upon invocation, place the attribute on that descendent:
<popup>
<button autofocus>My cool button</button>
</popup>
These autofocus
rules will be processed each time show
is called, as opposed to initial document load.
delegatesfocus
Section titled delegatesfocusSome authors may need to automatically focus the popup’s first focusable descendent, and may not wish to write script to determine at runtime which element that is. In such cases the delegatesfocus
attribute can be applied to the popup:
<popup delegatesfocus>
<p>I am not a focusable element.</p>
<p>Nor am I.</p>
<button>I will be focused whenever the popup becomes focused.</button>
</popup>
In the markup above, the button
element will receive focus any time the popup
would normally receive focus. For, example when popupElement.focus()
is called.
popup
supports an anchor
attribute which takes an ID reference to another element in the popup
’s owner document. The anchor
attribute is significant in that:
popup
and its anchor element separate from the DOM hierarchy. The hierarchy determines if a popup
is a descendant of another popup
. A descendant popup
does not dismiss its ancestor popup
s when shown.By default, popup
has a fixed position in the top-left corner of the viewport. With many popup use cases, however, authors need to be able to pin the position of one element to another element in the document; this is the case with our popup menu. Absolute positioning schemes sometimes suffice for this purpose, but require specific DOM structures and provide no functionality for repositioning.
We will soon make an additional proposal for a CSS anchored positioning scheme, which can be applied to popup
and other top-layer, interactive elements. For now, it is worth noting that a popup
’s anchor element (the element it will be “pinned” to) can be expressed using a new anchor
attribute on the popup:
<button id="myButton">Anchor element</button>
<popup open anchor="myButton">
<p>
<strong>New!</strong>
I’m some sort of educational UI…
</p>
…
</popup>
Note: for many popup
s, the element which invokes the popup
and the element the popup
is anchored to will be one and the same. However, there are cases where the author may want to anchor to a child/parent of the element which invoked the popup. Similiarly, there are cases (such as this teaching UI example) where no such invoking element exists. Therefore, we do not propose collapsing invocation and anchoring responsibilities to one attribute, as they are distinct responsibilities. In cases where the anchor
attribute is unset, but there is an associated invoking element, we could explore treating this as the anchor element. There would be complexities to think through if more than one element is associated to a popup
via the popup
attribute, or if the anchor
association causes reordering of trees (refer to Open Questions).
popup
Section titled Dismissing%20the%20popupopen
attributeSection titled Removing%20the%20open%20attributeRecall that authors can apply the open
attribute in order to show the popup
:
<popup open>
<p>
<strong>New!</strong>
I’m some sort of educational UI…
</p>
…
</popup>
Removing the attribute will dismiss the popup
.
All other following methods of dismissing the popup
will automatically remove the open
attribute from the popup
.
popup
attributeSection titled The%20popup%20attributeWhen the popup
was shown as a result of user interaction on an element with the popup
attribute…
<button id="menuButton" popup="menuPopup">Menu</button>
<popup id="menuPopup" role="menu" anchor="menuButton">
<!-- Markup for menuitems goes here -->
</popup>
…repeating/reversing that action will dismiss the popup. In this example, invoking the button
again when the popup
is visible will hide the popup
. Moving focus from input type="text"
(so long as focus does not then move to the popup
) will hide an associated popup
.
hide()
methodSection titled The%20hide%28%29%20methodA popup
can be hidden by calling the hide()
method:
// Author calls hide() according to some app logic, such as choosing a menu item
document.getElementById('menuPopup').hide()
The popup
may also be implicitly dismissed due to user interactions which trigger light dismissal. When dismissal occurs:
popup
is removed from the browser-managed, top-layer stack so that it is no longer rendered.hide
event is fired on the popup
when the popup
is hidden in response to light dismissal.An opened popup
will have “light dismiss” behavior, meaning that it will be automatically hidden when:
popup
or its anchor element is changed.popup
(and its invoking and anchor elements, if applicable).A generalized definition of “light dismiss” is being developed at Open UI.
select
controlSection titled Applicability%20to%20the%20select%20controlWhile some popup
s may be entire components in and of themselves, other popup
s may be a part of a larger whole. For example, the native select
element includes a popup
(sometimes rendered as a wheel) to present option
s to the user:
Per “Enabling Custom Control UI”, authors should be able to customize parts of a native control, including the select
popup
. While we anticipate discussing the anatomy of a select
in depth in the Open UI venue, any solution for providing arbitrary popup
s will also be applied to the select
’s Shadow DOM.
For example, popup
may be used in the select
Shadow DOM like so:
<template>
<slot name="entire">
<slot name="button-wrap">
<div part="button">
<slot name="selected-value-wrap">
<div part="selected-value"></div>
</slot>
</div>
</slot>
<slot name="listbox-wrap">
<popup part="listbox">
<slot>
<!-- All the options will end up in this unnamed slot if none of the parent slots are replaced by authors -->
</slot>
</popup>
</slot>
</slot>
</template>
If it fits their use case, an author could then entirely replace this listbox with their own popup
in markup:
<select>
<div slot="button-container" part="button" style="display: flex;">
<div part="selected-value-wrap">Option One</div>
<svg>... arrow down svg ..</svg>
</div>
<popup class="my-custom-listbox" slot="listbox-wrap" part="listbox">
<!-- Contents of the select popup -->
<option>Option One</option>
<option>Option Two</option>
<details>
<summary>Show more options</summary>
<option>Option Three</option>
<option>Option Four</option>
</details>
</popup>
</select>
Freedom over the size and position of a popup
could enable an author to spoof UI belonging to the browser or documents other than the popup
’s document. For this reason the popup
will be constrained as all other elements of the relevant document are, by clipping the element to the document’s layout viewport.
dialog
element with popup
-specific methods and new options. This option wasn’t pursued because it would result in a “mashed-up” API surface and collection of behaviors that seem better separate than together. Here are some examples of the semantic differences between the two elements to illustrate the point:popup
s have lightweight UI that dismises automatically when the user interacts with other UI, or when a task is completed within the popup
(such as selecting an option).dialogs
are more persistent and are generally dismissed explicitly by the user.popup
can be shown at a time.dialog
can be presented at a time.dialog
can be modal, such that user interaction with other UX elements is prevented.dialog
will dismiss a popup
when shown but the converse isn’t true.type
attribute for popup
, which would provide a set of default styles, user input behaviors, and accessibility semantics for various classes of popups. However, with the availability of the proposed HTML attributes and CSS property values, this approach did not provide much added authoring value past platform-managed accessibility semantics to the parent popup. Because this approach did not provide accessibility semantics or input behaviors for popup descendents, the authoring story was unclear in cases where the type of popup (e.g. type="menu"
) must contain particular descendents whose semantics could only be managed through ARIA (role="menuitem"
) unless a new mechanism was proposed.Collision with CSS contain. It’s worth noting that using the contain
CSS property on an ancestor of popup
will prevent popup
from being positioned and painted correctly. How common is this use case? How might the platform resolve this unintentional effect?
Could we require popups to use the DOM hierarchy for event propagation and establishing hierarchical popup relationships? Elements used for popup UI today are frequently appended to the end of the DOM to ensure they appear on top of other UI. With the new capabilities of the popup
element, that isn’t necessary, yet we still assume in this proposal that DOM positioning of the popup
needs to be separate from the anchor. One reason why that might still be needed is for anchor elements that can’t accept a popup
descendant, for example, image or input elements or custom-elements that expect a particular content model. Eliminating this requirement would also eliminate the complexity to modify the event propagation path based on the anchor attribute, and would make hierarchical relationships between popups clear just by observing the DOM hierarchy.
Should one of the attributes hoist up a popup
in trees (e.g. accessibility trees)? Today, it is common practice to include popup UI as a direct child of the root node. This practice is a workaround for top-layer positioning issues, and it is our hope that this proposal renders this practice obsolete. However, there might still be cases where an author includes a popup
in a separate point in the DOM to its anchor/invoking element(s). We may want to explore reordering trees such that popup
is moved into the proper context. Should the popup
attribute and/or anchor
attribute cause this reordering? What happens if both these attributes are present but refer to different elements? What happens if multiple popup
attributes refer to the same popup
element?
Show/hide or show/close? This proposal introduces a symmetrical show
/hide
method pair on the proposed popup
element. dialog
, however, sets a precedent for a show
and close
method pairing. That seemed less intuitive, but perhaps the existing pattern should be followed. Alternatively, the platform could introduce hide
on dialog
and consider close
deprecated.
Focus trapping: the inert
attribute enables authors to mark portions of a document as inert, such that user input events are ignored. Inverting this model, new primitives could enable focus trapping with parts of a document, e.g. a popup
. New focus trapping primitives could be useful in cases where the tab cycle should be constrained to the popup
, but the rest of the document would receive other types of user input.
Animating state transitions: applying animations and transitions to interactive elements’ show/hide states can be difficult. For example, to apply a CSS transition the element must first produce an initial box before its properties can be transitioned to new values. That requires a two step process to first show the element, and in a subsequent frame, initiate a transition by applying a class. Likewise, since the browser manages the visibility of the popup for light dismiss behaviors, it is impossible to apply a close animation. To address this issue perhaps the answer is to invent a new CSS animation primitive that is triggered when an element stops producing a box.
hidden
attributeSection titled The%20hidden%20attributeThis proposal specifies that, similarly to the dialog
element, the open
attribute can be used to show a popup
. Currently, authors are advised to add a hidden
attribute to dialog
when hiding it, as there are some quirks with removing the open
attribute. Rather than porting over this behavior to popup
, it would be ideal to adjust the behavior on dialog
. As a result and to provide simpler authoring, we are proposing that authors solely remove/add the open
attribute in order to toggle visibility of a popup
, as opposed to introducing the hidden
attribute to this new element.
In a previous version of this document, we proposed that the hierarchy created by the anchor
attribute relationship affects the event propagation path. With the introduction of a separate popup
attribute which creates an invocation relationship, it is less clear whether event bubbling should be changed as a result of the popup
attribute and/or the anchor
attribute.
This behavior as previously proposed adds complexity to the platform, and it is not clear whether there is enough value to authors for the platform to take on that complexity. We welcome feedback on this point and preserve the previous proposal here.
In the markup below, a table with many rows is rendered, each of which displays a custom popup filled with commands when right-clicked. The popup is defined once and its anchor attribute is adjusted so that it is shown aligned with the table row when the contextmenu event is received.
After a command is selected from the menu, the menu dispatches a custom command event which is then handled by the table row. The table row receives the event even though it isn’t a DOM ancester of the popup. This is because the event target parent of a popup is its anchor element.
<table id="work-table">
<tr data-type="task">...</tr>
<tr data-type="bug">...</tr>
<tr data-type="task">...</tr>
...
</table>
<popup id="bug-commands">
<button id="command1" onclick="dispatchCommandEvent(event)">command 1</button>
<button id="command2" onclick="dispatchCommandEvent(event)">command 2</button>
...
</popup>
<script type="module">
class CommandEvent extends CustomEvent {
constructor(name) {
super("command", { detail: name })
}
}
function dispatchCommandEvent(e) {
e.stopPropagation()
e.currentTarget.dispatchEvent(new CommandEvent(e.currentTarget.id))
}
let bugCommands = document.querySelector("#bug-commands")
let bugs = document.querySelectorAll("[data-type=bug]")
for (let bug of bugs) {
bug.addEventListener("command", handleBugCommand)
bug.addEventListener("contextmenu", showBugCommands)
}
function handleBugCommand(e) {
...
}
function showBugCommands(e) {
bugCommands.anchor = e.currentTarget
bugCommands.show()
e.preventDefault()
}
</script>
Note: if event bubbling remains unchanged by the anchor
attribute, authors in this case would need to query for the popup
’s anchor element and dispatch the event from that element. So, e.currentTarget.dispatchEvent(new CommandEvent(e.currentTarget.id))
becomes bugCommands.anchor.dispatchEvent(new CommandEvent(e.currentTarget.id))
.