sp-overlay

Overview API Changelog

Overview

Section titled Overview

An <sp-overlay> element is used to decorate content that you would like to present to your visitors as "overlaid" on the rest of the application. This includes dialogs (modal and not), pickers, tooltips, context menus, et al.

Usage

Section titled Usage

See it on NPM! How big is this package in your project?

yarn add @spectrum-web-components/overlay

Import the side effectful registration of <sp-overlay> as follows:

import '@spectrum-web-components/overlay/sp-overlay.js';

When looking to leverage the Overlay base class as a type and/or for extension purposes, do so via:

import { Overlay } from '@spectrum-web-components/overlay';

Example

Section titled Example

By leveraging the trigger attribute to pass an ID reference to another element within the same DOM tree, your overlay will be positioned in relation to this element. When the ID reference is followed by an @ symbol and interaction type, like click, hover, or longpress, the overlay will bind itself to the referenced element via the DOM events associated with that interaction.

<sp-button id="trigger">Overlay Trigger</sp-button>

<!-- Opening an overlay via a click interaction -->
<sp-overlay trigger="trigger@click" placement="bottom">
    <sp-popover>
        <sp-dialog>
            <h2 slot="heading">Clicking opens this popover...</h2>
            <p>But, it really could be anything. Really.</p>
        </sp-dialog>
    </sp-popover>
</sp-overlay>

<!-- Opening an overlay via a hover interaction -->
<sp-overlay trigger="trigger@hover" placement="bottom">
    <sp-tooltip>
        I'm a tooltip and I'm triggered by hovering over the button!
    </sp-tooltip>
</sp-overlay>

Anatomy

Section titled Anatomy

When a <sp-overlay> element is opened, it will pass that state to its direct children elements as the property open, which it will set to true. Elements should react to this by initiating any transition between closed and open that they see fit. Similarly, open will be set to false when the <sp-overlay> element is closed.

Options

Section titled Options
delayed

An Overlay that is delayed will wait until a warm-up period of 1000ms has completed before opening. Once the warmup period has completed, all subsequent Overlays will open immediately. When no Overlays are opened, a cooldown period of 1000ms will begin. Once the cooldown has completed, the next Overlay to be opened will be subject to the warm-up period if provided that option.

<sp-button id="trigger">Overlay Trigger</sp-button>

<sp-overlay trigger="trigger@hover" placement="bottom" delayed>
    <sp-tooltip>I'm a tooltip and I'm delayed!</sp-tooltip>
</sp-overlay>
notImmediatelyCloseable offset placement receivesFocus

trigger

Section titled trigger

The trigger option accepts an HTMLElement or a VirtualTrigger from which to position the Overlay.

  • You can import the VirtualTrigger class from the overlay package to create a virtual trigger that can be used to position an Overlay. This is useful when you want to position an Overlay relative to a point on the screen that is not an element in the DOM, like the mouse cursor.
type

The type of an Overlay outlines a number of things about the interaction model within which it works:

Modal

'modal' Overlays create a modal context that traps focus within the content and prevents interaction with the rest of the page. The overlay manages focus trapping and accessibility features like aria-modal="true" to ensure proper screen reader behavior.

They should be used when you need to ensure that the user has interacted with the content of the Overlay before continuing with their work. This is commonly used for dialogs that require a user to confirm or cancel an action before continuing.

<sp-button id="trigger">open modal</sp-button>
<sp-overlay trigger="trigger@click" type="modal">
    <sp-dialog-wrapper headline="Signin form" dismissable underlay>
        <p>I am a modal type overlay.</p>
        <sp-field-label>Enter your email</sp-field-label>
        <sp-textfield placeholder="test@gmail.com"></sp-textfield>
        <sp-action-button
            onClick="
                this.dispatchEvent(
                    new Event('close', {
                        bubbles: true,
                        composed: true,
                    })
                );
            "
        >
            Sign in
        </sp-action-button>
    </sp-dialog-wrapper>
</sp-overlay>
Page Hint Auto Manual

Events

Section titled Events

When fully open the <sp-overlay> element will dispatch the sp-opened event, and when fully closed the sp-closed event will be dispatched. Both of these events are of type:

type OverlayStateEvent = Event & {
    overlay: Overlay;
};

The overlay value in this case will hold a reference to the actual <sp-overlay> that is opening or closing to trigger this event. Remember that some <sp-overlay> element (like those creates via the imperative API) can be transiently available in the DOM, so if you choose to build a cache of Overlay elements to some end, be sure to leverage a weak reference so that the <sp-overlay> can be garbage collected as desired by the browser.

When it is "fully" open or closed?

Section titled When it is "fully" open or closed?

"Fully" in this context means that all CSS transitions that have dispatched transitionrun events on the direct children of the <sp-overlay> element have successfully dispatched their transitionend or transitioncancel event. Keep in mind the following:

  • transition* events bubble; this means that while transition events on light DOM content of those direct children will be heard, those events will not be taken into account
  • transition* events are not composed; this means that transition events on shadow DOM content of the direct children will not propagate to a level in the DOM where they can be heard

This means that in both cases, if the transition is meant to be a part of the opening or closing of the overlay in question you will need to redispatch the transitionrun, transitionend, and transitioncancel events from that transition from the closest direct child of the <sp-overlay>.

Integration patterns

Section titled Integration patterns

Action bar system

Section titled Action bar system
<style>
    .overlay-demo-popover sp-action-group {
        padding: var(--spectrum-actiongroup-vertical-spacing-regular);
    }
    #overlay-demo {
        position: static;
    }
    #overlay-demo:not(:defined),
    #overlay-demo *:not(:defined) {
        display: none;
    }
</style>
<sp-popover id="overlay-demo" class="overlay-demo-popover" open>
    <sp-action-group vertical quiet emphasized selects="single">
        <sp-action-button id="trigger-1" hold-affordance>
            <sp-icon-anchor-select slot="icon"></sp-icon-anchor-select>
        </sp-action-button>
        <sp-action-button id="trigger-2" hold-affordance>
            <sp-icon-polygon-select slot="icon"></sp-icon-polygon-select>
        </sp-action-button>
        <sp-action-button id="trigger-3" hold-affordance>
            <sp-icon-rect-select slot="icon"></sp-icon-rect-select>
        </sp-action-button>
    </sp-action-group>
    <sp-overlay trigger="trigger-1@hover" type="hint">
        <sp-tooltip>Hover</sp-tooltip>
    </sp-overlay>
    <sp-overlay
        trigger="trigger-1@longpress"
        type="auto"
        placement="right-start"
    >
        <sp-popover class="overlay-demo-popover" tip>
            <sp-action-group vertical quiet>
                <sp-action-button>
                    <sp-icon-anchor-select slot="icon"></sp-icon-anchor-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-polygon-select
                        slot="icon"
                    ></sp-icon-polygon-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-rect-select slot="icon"></sp-icon-rect-select>
                </sp-action-button>
            </sp-action-group>
        </sp-popover>
    </sp-overlay>
    <sp-overlay trigger="trigger-2@hover" type="hint">
        <sp-tooltip>Hover</sp-tooltip>
    </sp-overlay>
    <sp-overlay
        trigger="trigger-2@longpress"
        type="auto"
        placement="right-start"
    >
        <sp-popover class="overlay-demo-popover" tip>
            <sp-action-group vertical quiet>
                <sp-action-button>
                    <sp-icon-anchor-select slot="icon"></sp-icon-anchor-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-polygon-select
                        slot="icon"
                    ></sp-icon-polygon-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-rect-select slot="icon"></sp-icon-rect-select>
                </sp-action-button>
            </sp-action-group>
        </sp-popover>
    </sp-overlay>
    <sp-overlay trigger="trigger-3@hover" type="hint">
        <sp-tooltip>Hover</sp-tooltip>
    </sp-overlay>
    <sp-overlay
        trigger="trigger-3@longpress"
        type="auto"
        placement="right-start"
    >
        <sp-popover class="overlay-demo-popover" tip>
            <sp-action-group vertical quiet>
                <sp-action-button>
                    <sp-icon-anchor-select slot="icon"></sp-icon-anchor-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-polygon-select
                        slot="icon"
                    ></sp-icon-polygon-select>
                </sp-action-button>
                <sp-action-button>
                    <sp-icon-rect-select slot="icon"></sp-icon-rect-select>
                </sp-action-button>
            </sp-action-group>
        </sp-popover>
    </sp-overlay>
</sp-popover>

Advanced topics

Section titled Advanced topics

API

Section titled API
<sp-overlay
    ?open=${boolean}
    ?delayed=${boolean}
    offset=${Number | [Number, Number]}
    placement=${Placement}
    receives-focus=${'true' | 'false' | 'auto' (default)
    trigger=${string | ${string}@${string}}
    .triggerElement=${HTMLElement}
    .triggerInteraction=${'click' | 'longpress' | 'hover'}
    type=${'auto' | 'hint' | 'manual' | 'modal' | 'page'}
></sp-overlay>
API value interactions
Section titled API value interactions

When a triggerElement is present (via trigger attribute or direct property setting), the following configurations apply:

Configuration Required Properties Behavior Basic Placement `placement` + `triggerElement` Content positions next to trigger Placement + Offset `placement` + `offset` + `triggerElement` Content positions with extra spacing Invalid Placement `placement` without `triggerElement` No positioning occurs No Placement No `placement` specified Content positioning handled by: • Content itself • Application

Common in modal/page overlays for full-screen content

Styling

Section titled Styling

<sp-overlay> element will use the <dialog> element or popover attribute to project your content onto the top-layer of the browser, without being moved in the DOM tree. That means that you can style your overlay content with whatever techniques you are already leveraging to style the content that doesn't get overlaid. This means standard CSS selectors, CSS Custom Properties, and CSS Parts applied in your parent context will always apply to your overlaid content.

Top layer over complex CSS

Section titled Top layer over complex CSS

There are some complex CSS values that have not yet been covered by the positioning API that the <sp-overlay> element leverages to associate overlaid content with their trigger elements. In specific, properties like filter, when applied to a trigger element within which lives the related content to be overlaid, are not currently supported by the relationship created herein. If support for this is something that you and the work you are addressing would require, we'd love to hear from you in an issue. We'd be particularly interested in speaking with you if you were interested in contributing support/testing to ensure this capability for all consumers of the library.

Fallback support

Section titled Fallback support

While the <dialog> element is widely supported by browsers, the popover attribute is still quite new. When leveraged in browsers that do not yet support the popover attribute, there may be additional intervention required to ensure your content is delivered to your visitors as expected.

Complex layered
Section titled Complex layered

When an overlay is placed within a page with complex layering, the content therein can fall behind other content in the z-index stack. The following example is somewhat contrived but, imagine a toolbar next to a properties panel. If the toolbar has a lower z-index than the properties panel, any overlaid content (tooltips, etc.) within that toolbar will display underneath any content in the properties panel with which it may share pixels.

<div class="complex-layered-demo">
    <div class="complex-layered-holder">
        <sp-action-button id="complex-layered">Trigger</sp-action-button>
        <sp-overlay
            trigger="complex-layered@hover"
            type="hint"
            placement="bottom-start"
        >
            <sp-tooltip>
                I can be partially blocked when [popover] is not available
            </sp-tooltip>
        </sp-overlay>
    </div>
    <div class="complex-layered-blocker"></div>
</div>
<style>
    .complex-layered-demo {
        position: relative;
    }
    .complex-layered-holder {
        z-index: 1;
        position: relative;
    }
    .complex-layered-blocker {
        position: relative;
        z-index: 10;
        background: white;
        width: 100%;
        height: 40px;
    }
</style>

Properly managed z-index values will support working around this issue while browsers work to adopt the popover attribute. In this demo, you can achieve the same output by sharing one z-index between the various pieces of content, removing z-index values altogether, or raising the .complex-layered-holder element to a higher z-index than the .complex-layered-blocker element.

Contained
Section titled Contained

CSS Containment gives a developer direct control over how the internals of one element affect the paint and layout of the internals of other elements on the same page. While leveraging some of its values can offer performance gains, they can interrupt the delivery of your overlaid content.

<div class="contained-demo">
    <sp-action-button id="contained">Trigger</sp-action-button>
    <sp-overlay trigger="contained@hover" type="hint" placement="bottom-start">
        <sp-tooltip>
            I can be blocked when [popover] is not available
        </sp-tooltip>
    </sp-overlay>
</div>
<style>
    .contained-demo {
        contain: content;
    }
</style>

You could just remove the contain rule. But, if you are not OK with simply removing the contain value, you still have options. If you would like to continue to leverage contain, you can place your "contained" content separately from your overlaid content, like so:

<div class="contained-demo">
    <sp-action-button id="contained-working">Trigger</sp-action-button>
</div>
<sp-overlay
    trigger="contained-working@hover"
    type="hint"
    placement="bottom-start"
>
    <sp-tooltip>I can be blocked when [popover] is not available</sp-tooltip>
</sp-overlay>
<style>
    .contained-demo {
        contain: content;
    }
</style>

<sp-overlay> accepts an ID reference via the trigger attribute to relate it to interactions and positioning in the DOM. To fulfill this reference the two elements need to be in the same DOM tree. However, <sp-overlay> alternatively accepts a triggerElement property that opens even more flexibility in addressing this situation.

Clip pathed
Section titled Clip pathed

clip-path can also restrict how content in an element is surfaced at paint time. When overlaid content should display outside of the clip-path, without the popover attribute, that content could be clipped.

<div class="clip-pathed-demo">
    <sp-action-button id="clip-pathed">Trigger</sp-action-button>
    <sp-overlay
        trigger="clip-pathed@hover"
        type="hint"
        placement="bottom-start"
    >
        <sp-tooltip>
            I can be blocked when [popover] is not available
        </sp-tooltip>
    </sp-overlay>
</div>
<style>
    .clip-pathed-demo {
        clip-path: inset(0 0);
    }
</style>

Here, again, working with your content needs (whether or not you want to leverage clip-path) or DOM structure (not colocating clipped and non-clipped content) will allow you to avoid this issue:

<div class="clip-pathed-demo">
    <sp-action-button id="clip-pathed-working">Trigger</sp-action-button>
</div>
<sp-overlay
    trigger="clip-pathed-working@hover"
    type="hint"
    placement="bottom-start"
>
    <sp-tooltip>I can be blocked when [popover] is not available</sp-tooltip>
</sp-overlay>
<style>
    .clip-pathed-demo {
        clip-path: inset(0 0);
    }
</style>
Non-overflowing, relative containers with z-index in Safari
Section titled Non-overflowing, relative containers with z-index in Safari

Under very specific conditions, WebKit will incorrectly clip fixed-position content. WebKit clips position: fixed elements within containers that have all of:

  1. position: relative
  2. overflow: clip or overflow: hidden
  3. z-index greater than zero

If you notice overlay clipping only in Safari, this is likely the culprit. The solution is to break up the conditions into separate elements to avoid triggering WebKit's bug. For example, leaving relative positioning and z-index on the outermost container while creating an inner container that enforces the overflow rules.

Accessibility

Section titled Accessibility

Nested overlays

Section titled Nested overlays

When nesting multiple overlays, it is important to ensure that the nested overlays are actually nested in the HTML as well, otherwise it will not be accessible.

<div style="padding: 20px;">
    <sp-button id="outerTrigger" variant="primary" aria-haspopup="dialog">
        Open Outer Modal
    </sp-button>
    <sp-overlay id="outerOverlay" type="auto" trigger="outerTrigger@click">
        <sp-popover>
            <sp-dialog>
                <h2 slot="heading" id="outer-dialog-heading">Outer Dialog</h2>
                <p>This is the outer modal content. Press ESC to close it.</p>
                <sp-button
                    id="innerTrigger"
                    variant="primary"
                    aria-haspopup="dialog"
                >
                    Open Inner Modal
                </sp-button>
                <sp-overlay
                    id="innerOverlay"
                    type="auto"
                    trigger="innerTrigger@click"
                >
                    <sp-popover>
                        <sp-dialog>
                            <h2 slot="heading" id="inner-dialog-heading">
                                Inner Dialog
                            </h2>
                            <p>
                                This is the inner modal content. Press ESC to
                                close this first, then the outer modal.
                            </p>
                        </sp-dialog>
                    </sp-popover>
                </sp-overlay>
            </sp-dialog>
        </sp-popover>
    </sp-overlay>
</div>

Focus management

Section titled Focus management

The overlay manages focus based on its type:

  • For modal and page types, focus is always trapped within the overlay
  • For auto and manual types, focus behavior is controlled by the receives-focus attribute
  • For hint type, focus remains on the trigger element

Example of proper focus management:

<sp-button id="modal-trigger" aria-haspopup="dialog" aria-expanded="false">
    Open Settings
</sp-button>
<sp-overlay trigger="modal-trigger@click" type="modal">
    <sp-dialog-wrapper
        headline="Settings"
        dismissable
        underlay
        aria-labelledby="settings-heading"
    >
        <h2 id="settings-heading" slot="heading">Settings</h2>
        <sp-field-label for="setting1">Email Notifications</sp-field-label>
        <sp-switch id="setting1">Enable notifications</sp-switch>

        <div slot="footer">
            <sp-button
                variant="secondary"
                onclick="this.dispatchEvent(new Event('close', { bubbles: true }))"
            >
                Cancel
            </sp-button>
            <sp-button
                variant="accent"
                onclick="this.dispatchEvent(new Event('close', { bubbles: true }))"
            >
                Save
            </sp-button>
        </div>
    </sp-dialog-wrapper>
</sp-overlay>

Keyboard navigation

Section titled Keyboard navigation
Key Action ESC Closes overlays in reverse order of opening TAB/Shift+TAB Navigates through focusable elements within modal/page overlays Arrow keys Navigate through menu items in menu overlays ENTER/SPACE Activates buttons and controls

Screen reader considerations

Section titled Screen reader considerations
  • Use aria-haspopup on trigger elements to indicate the type of overlay
  • Provide descriptive labels using aria-label or aria-labelledby
  • Use proper heading structure within overlays
  • Ensure error messages are announced using aria-live

Example of a tooltip with proper screen reader support:

<sp-button id="help-trigger" aria-describedby="help-tooltip" label="Help">
    <sp-icon-help slot="icon"></sp-icon-help>
</sp-button>
<sp-overlay trigger="help-trigger@hover" type="hint">
    <sp-tooltip id="help-tooltip">
        Click for more information about this feature
    </sp-tooltip>
</sp-overlay>