| Version | Tag | Status | API |
|---|---|---|---|
| v1 | <oc-popover-v1> |
Stable, allowed for generation | PopoverV1 |
Overview
Source: ./src/components/popover/Overview.mdx
Popover
The popover component provides small non-modal dialogs that display additional contextual information, hints or menus related to a specific element on the page.
Popovers are typically triggered by user interactions such as clicking or hovering over an element.
Default variation
Story Default:
<oc-popover-v1 oc-aria-label="Default popover">
Here is the Popover! It doesn't come with any padding by default.
</oc-popover-v1>
Configuration
The popover is configurable, allowing you to tailor its features and appearance to your specific needs.
Usage guidelines
Before integrating the popover component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations.
Info
See the Popover UX documentation for detailed user experience guidelines.
Note
For simple, attribute-driven tooltips, prefer
oc-tooltip-v2over thetooltipvariant of the popover. Only use thetooltipvariant of the popover when you need popover-specific features such as custom content, sticky behavior, or alongpress/clicktrigger.
The following guidelines apply to the popover component:
Anchoring
- Every Popover must have an anchor element to be displayed. The anchor element must exist in the DOM and be visible when the popover element is initialised.
- The anchor element must not have its own click handler or on-click behavior. This might lead to unexpected results.
- See the configuration page for more details on how to set the anchor element.
Content
- The default slot contains the content of the popover and can be used for any valid HTML content.
- The content can contain interactive elements (like buttons) for closing the popover with the
data-popover-close="click"attribute.
Accessibility
The popover component comes with a set of built-in accessibility features to ensure a seamless experience for all users.
It follows the interaction model of a non-modal dialog, as defined by the dialog ARIA role.
Labeling
The oc-aria-label attribute sets the aria-label attribute on the popover content element for better accessibility.
If not set, the popover is less accessible to screen readers, as it doesn't have a descriptive label to identify its purpose or content.
Keyboard navigation
The popover component supports standard keyboard navigation for interactive elements:
| Shortcut | Description |
|---|---|
| Tab | Moves focus to the popover trigger |
| Enter/Space | Opens the popover |
| Esc | Closes the popover when open |
Keyboard focus management
When the popover is opened via the keyboard, the focus moves to the close button. When the popover is subsequently closed again, the focus returns to the interactive element that opened it.
Focus handling
The popover does not trap the focus, meaning that users can navigate away from it using the Tab key.
Configuration
Source: ./src/components/popover/ConfigurationV1.mdx
Popover configuration
Configure the popover component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current component configuration.
Behaviour of popovers dependent on trigger and close-on type
Popovers support two types of triggers to open the popover: click and mouseenter and corresponding
close-on actions to close the popover: click and mouseleave.
By default, if no trigger is set, the popover opens on click and closes on outside click or Esc key press.
If a trigger is set, the popover uses a default close behaviour as described in the table below.
| trigger | onClose behaviour. |
|---|---|
click |
closes on outside click and Esc key press |
mousenter |
closes on mouse leave |
longpress |
closes automatically, but also on outside click and Esc key press |
Note:
mouseenterandmouseleaveare only supported for pointer devices and not for keyboard navigation or touch devices.longpressis only supported for touch devices.
Anchors
Every Popover must have an anchor element that serves as the reference point for its position. A popover without an anchor element can not be shown.
You can specify the anchor element using the anchor attribute, which accepts a CSS selector string, an HTMLElement,
or the string "previous-sibling", which uses the previous sibling element as the anchor.
If no anchor is specified, the popover does not appear on screen.
The anchor element must not have its own click handler or on-click behavior. This might lead to unexpected results.
Styling
Styling of the popover component can be achieved by overriding
the --oc-popover... CSS variables, see list on this page below.
If any of the above properties are not set, the component falls back to decent default values.
Story Default:
<oc-popover-v1 oc-aria-label="Default popover">
Here is the Popover! It doesn't come with any padding by default.
</oc-popover-v1>
Interactive configurator (Storybook controls); every option is listed in the API section of this file.
API v1
Source: ./src/components/popover/v1/PopoverV1.API.g.mdx
Popover v1 API
API: <oc-popover-v1> (PopoverV1)
The toggletip component provides a small non-modal dialog box that displays additional contextual information or hints when clicking an interactive element. It supports custom content and offers configuration options for positioning either above or below the triggering element, as well as controlling its initial visibility state.
Attributes / properties
| Attribute | Type | Default | Required | Description |
|---|---|---|---|---|
variant |
"flyout" | "toggletip" | "tooltip" |
"flyout" |
no | Sets the variant of the popover. Choosing a variant defines the default styling as well as the trigger and close behavior. Note For simple, attribute-driven tooltips, prefer oc-tooltip-v2 over the tooltip variant. Only use the tooltip variant when you need popover-specific features such as custom content, sticky behavior, or a longpress/click trigger. |
sticky |
boolean |
false |
no | Sets the sticky state of the popover. Set to true an opened popover always stays in viewport, even if the users scrolls the page. This attribute is automatically unset on closing the popover. |
visible |
boolean |
false |
no | Sets the visibility state of the popover. Set to true to show the popover. This attribute is automatically unset on closing the popover. |
anchor |
string | HTMLElement |
undefined |
no | Sets the CSS selector of the element to which the popover is anchored. You can also set the anchor element directly by passing an HTMLElement instead of a selector string. If you pass the special string previous-sibling, the popover will be anchored to the direct previous sibling element.If there is no anchor set, the popover is not anchored to any element and does not open until you set the anchor property to a valid element or selector. |
close-button |
boolean |
false |
no | Sets the presence of a close button within the popover. If set to true, a standard close button is rendered inside the popover content, which can be used to close the popover when clicked. Note: it is always possible to add custom close buttons within the popover content by adding the data-popover-close="click" attribute to any element. |
trigger |
"none" | "click" | "mouseenter" | "longpress" | "focus" |
"click" |
no | Sets the trigger action that opens the popover. This can also be a space-separated list of multiple triggers, e.g. "click mouseenter". If not given, the popover opens on click. |
close-on |
"none" | "click" | "mouseleave" | "blur" |
"click" for "click" trigger, "mouseleave" for "mouseenter" trigger |
no | Sets the possible close action that closes the popover. This can also be a space-separated list of multiple close actions, e.g. "click mouseleave". If not given, the trigger defines the close action as well. |
position |
"top" | "bottom" |
"top" |
no | Sets the preferred position of the popover. The position can change depending on the available space. |
oc-aria-label |
string |
undefined |
no | Sets the aria-label attribute on the popover content element for better accessibility. If not set, a default label of "Popover" is used. |
Slots
| Slot | Required | Description |
|---|---|---|
default |
yes | Contains the content of the popover itself. Example: <span>This is the popover!</span> |
Events
| Event | Detail type | Description |
|---|---|---|
oc-popover-open |
CustomEvent<void> |
Dispatched when the popover is being opened. May be canceled to prevent the popover from opening. |
oc-popover-close |
CustomEvent<void> |
Dispatched when the popover is being closed. May be canceled to prevent the popover from closing. |
oc-property-change |
OcPopoverV1Events["oc-property-change"] |
Whenever a property value changes, this event triggers. Use this event to track all property changes within the component. Refer to the Events documentation for more information. |
oc-mount |
{ component: string; } |
Fired when the component is mounted to the DOM. The event is fired when the onMount hook of the component is called by the runtime. |
oc-unmount |
{ component: string; } |
Fired when the component is unmounted from the DOM. The event is fired when the function returned by the onMount hook of the component is called by the runtime. |
Methods
recalcPosition:() => voidTrigger a manual recalculation of the popover's position. This can be useful if the content of the popover changes dynamically while it's open, or if the size. Note: The popover automatically recalculates its position when opened, so in most cases you don't need to call this method manually.
Returns:
voidExample:
const popover = document.querySelector("oc-popover-v1"); popover.recalcPosition();
CSS custom properties
| Custom property | Default | Description |
|---|---|---|
--background-color |
Sets the background color of the popover bubble and arrow. No gradients or transparency supported. | |
--padding |
Sets the padding of the popover content area. | |
--border-radius |
Sets the border radius of the popover bubble. | |
--min-width |
Sets the minimum width of the popover. | |
--max-width |
Sets the maximum width of the popover. | |
--arrow-height |
Sets the height of the popover arrow. The width of the arrow is calculated based on the height to maintain a consistent aspect ratio. |
Variations
Source: ./src/components/popover/VariationsV1.mdx
Variations
Listed below are the most common variations of the popover component as well as specific component variations for different use cases.
You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.
Default
Story: components-popover-variations--default · tags: components
The default configuration initially hides the popover and displays it below the anchor element. Be aware that popovers come without any padding by default, so the content is directly adjacent to the border and the arrow. Adding padding with CSS variables is recommended for better readability (see other examples).
Args: oc-aria-label=Default popover, defaultSlot=Here is the Popover! It doesn't come with any padding by default.
<oc-popover-v1 oc-aria-label="Default popover">
Here is the Popover! It doesn't come with any padding by default.
</oc-popover-v1>
With Padding
Story: components-popover-variations--with-padding · tags: components
Showcases how to add padding to the popover content by using the --padding CSS variable.
Args: defaultSlot=Padding can be added with CSS variables:<br />--padding: 1rem;, oc-aria-label=Popover with padding, --padding=1rem
<oc-popover-v1 oc-aria-label="Popover with padding" style="--padding: 1rem">
Padding can be added with CSS variables:<br />--padding: 1rem;
</oc-popover-v1>
As Toggletip
Story: components-popover-variations--as-toggletip · tags: components
Showcases how to add padding to the popover content by using the --padding CSS variable.
Args: defaultSlot=Decent styling for toggletips by choosing the 'toggletip' variant., oc-aria-label=Toggletip popover, variant=toggletip, close-button=true
<oc-popover-v1 oc-aria-label="Toggletip popover" variant="toggletip" close-button>
Decent styling for toggletips by choosing the 'toggletip' variant.
</oc-popover-v1>
As Toggletip with custom background color
Story: components-popover-variations--as-toggletip-with-custom-background-color · tags: components
Showcases how to add custom background-color to the popover component via --background-color css property
Args: defaultSlot=Decent styling for toggletips by choosing the 'toggletip' variant., oc-aria-label=Toggletip popover, variant=toggletip, close-button=true, --background-color=var(--oc-semantic-color-background-strong-purple)
<div
style="height: 150px; display: flex; align-items: center; justify-content: center; ${parameters.style}"
>
<oc-button-v1>Anchor Button</oc-button-v1>
<oc-popover-v1 anchor="previous-sibling" oc-aria-label="Toggletip popover" variant="toggletip" close-button style="--background-color: var(--oc-semantic-color-background-strong-purple)" class="demo-class">
Decent styling for toggletips by choosing the 'toggletip' variant.
</oc-popover-v1>
${css}
</div>
Story source (TypeScript, verbatim from Storybook)
{
name: "As Toggletip with custom background color",
args: {
defaultSlot: "Decent styling for toggletips by choosing the 'toggletip' variant.",
"oc-aria-label": "Toggletip popover",
variant: "toggletip",
"close-button": true,
"--background-color": "var(--oc-semantic-color-background-strong-purple)"
},
parameters: {},
render(args, {
parameters
}) {
// We need to exclude anchor to prevent it from being overwritten by storybook
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
defaultSlot,
anchor: _anchor,
...props
} = args;
const {
css
} = cssVariablesExample(args);
return html`<div
style="height: 150px; display: flex; align-items: center; justify-content: center; ${parameters.style}"
>
<oc-button-v1>Anchor Button</oc-button-v1>
<oc-popover-v1 anchor="previous-sibling" ${spread(props)} class="demo-class">
${unsafeHTML(defaultSlot)}
</oc-popover-v1>
${css}
</div>`;
}
}
Preferred position
Story: components-popover-variations--above · tags: components
Variation using the position attribute with the value top to set the preferred position of the popover above the anchor element.
The popover may still be displayed either below or above the anchor element depending on the available space.
Args: defaultSlot=Here is the Popover on top., oc-aria-label=Popover on top, position=top, --padding=1rem
<oc-popover-v1 oc-aria-label="Popover on top" position="top" style="--padding: 1rem">
Here is the Popover on top.
</oc-popover-v1>
Intially visible
Story: components-popover-variations--visible · tags: components
Variation with the visible attribute initially set to true, showing the popover without needing to click the anchor element.
Args: defaultSlot=<span>Here is the Popover!</span>, oc-aria-label=Initially visible popover, visible=true, --padding=1rem
<oc-popover-v1 oc-aria-label="Initially visible popover" visible style="--padding: 1rem">
<span>Here is the Popover!</span>
</oc-popover-v1>
Long text
Story: components-popover-variations--long-text · tags: components
Variation with long text, showcasing also a customized maximum width of the popover.
Args: oc-aria-label=Popover with long text, defaultSlot=(see snippet), --padding=1rem, --max-width=300px
<oc-popover-v1 oc-aria-label="Popover with long text" style="--padding: 1rem; --max-width: 300px">
<span>The text in this popover is very long. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.</span>
</oc-popover-v1>
Sticky on screen
Story: components-popover-variations--sticky-on-screen · tags: components
Variation that sticks on screen, even when anchor is scrolled out of the viewport.
Args: visible=true, sticky=true, trigger=none, defaultSlot=This Popover sticks in the viewport!, oc-aria-label=Sticky popover, --padding=1rem
<oc-popover-v1 visible sticky trigger="none" oc-aria-label="Sticky popover" style="--padding: 1rem">
This Popover sticks in the viewport!
</oc-popover-v1>
Customized styling
Story: components-popover-variations--customize · tags: components
A Demo showcasing a customized popover styling by overriding CSS variables on a container element. See configuration for a list of available CSS variables.
<div class="container">
<oc-link-v2 as-button id="link1">Click here for popover.</oc-link-v2>
<oc-popover-v1 position="bottom" anchor="#link1" oc-aria-label="Customized popover">
<span>This popover has custom styling</span>
<oc-button-v1 size="50" data-popover-close="click">Close</oc-button-v1>
</oc-popover-v1>
</div>
<style>
oc-popover-v1[anchor="#link1"] {
padding: 100px 10px;
--padding: 1rem 2rem;
--border-radius: 2rem;
--arrow-height: 1rem;
--background-color: #eeffff;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Customized styling",
argTypes: hideControlsBadge(Metadata),
parameters: {
controls: {
disabled: true
},
docs: {
story: {
inline: false,
height: "200px"
}
}
},
render() {
return html`
<div class="container">
<oc-link-v2 as-button id="link1">Click here for popover.</oc-link-v2>
<oc-popover-v1 position="bottom" anchor="#link1" oc-aria-label="Customized popover">
<span>This popover has custom styling</span>
<oc-button-v1 size="50" data-popover-close="click">Close</oc-button-v1>
</oc-popover-v1>
</div>
<style>
oc-popover-v1[anchor="#link1"] {
padding: 100px 10px;
--padding: 1rem 2rem;
--border-radius: 2rem;
--arrow-height: 1rem;
--background-color: #eeffff;
}
</style>
`;
}
}
Custom Close Button
Story: components-popover-variations--close-button · tags: components
Popovers can have custom close elements by adding the data-popover-close attribute to any element within
the popover content. Currently the only valid value for data-popover-close is "click", which means that the
popover closes when the element is clicked.
<div class="container">
<oc-link-v2 as-button id="link1">Click here for popover.</oc-link-v2>
<oc-popover-v1
position="bottom"
anchor="#link1"
oc-aria-label="Customized popover"
close-on="none"
>
<span>This popover can only be closed by clicking the button:</span>
<oc-button-v1 size="50" data-popover-close="click">Close</oc-button-v1>
</oc-popover-v1>
</div>
<style>
oc-popover-v1[anchor="#link1"] {
padding: 100px 10px;
--padding: 1rem 2rem;
--max-width: 200px;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Custom Close Button",
argTypes: hideControlsBadge(Metadata),
parameters: {
controls: {
disabled: true
},
docs: {
story: {
inline: false,
height: "200px"
}
}
},
render() {
return html`
<div class="container">
<oc-link-v2 as-button id="link1">Click here for popover.</oc-link-v2>
<oc-popover-v1
position="bottom"
anchor="#link1"
oc-aria-label="Customized popover"
close-on="none"
>
<span>This popover can only be closed by clicking the button:</span>
<oc-button-v1 size="50" data-popover-close="click">Close</oc-button-v1>
</oc-popover-v1>
</div>
<style>
oc-popover-v1[anchor="#link1"] {
padding: 100px 10px;
--padding: 1rem 2rem;
--max-width: 200px;
}
</style>
`;
}
}
Demo: Mein Konto
Story: components-popover-variations--demo-mein-konto · tags: components
<div class="container">
<oc-link-v2 as-button id="link1">Mein Konto</oc-link-v2>
<oc-popover-v1
position="bottom"
anchor="#link1"
oc-aria-label="Mein Konto"
class="mein-konto-popover"
>
<div class="header oc-px-100 oc-pb-75">
<h2 class="oc-headline-100">Mein Konto</h2>
<oc-icon-button-v3 icon="close" class="close"></oc-icon-button-v3>
</div>
<div class="oc-background-color-frame" style="min-height: 200px"></div>
<div class="actions oc-p-100 oc-gap-50 oc-mt-25">
<oc-button-v1 variant="primary">Anmelden</oc-button-v1>
<oc-button-v1 variant="secondary">Neu bei OTTO? Jetzt registrieren</oc-button-v1>
</oc-block-v2>
</oc-popover-v1>
</div>
<style>
.mein-konto-popover>.header {
padding-top: 20px;
display: flex;
justify-content: space-between;
}
.mein-konto-popover>.actions {
display: flex;
flex-direction: column;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo: Mein Konto",
parameters: {
controls: {
disabled: true
},
docs: {
disable: true
},
chromatic: {
hideInChromatic: true
}
},
render() {
return html`
<div class="container">
<oc-link-v2 as-button id="link1">Mein Konto</oc-link-v2>
<oc-popover-v1
position="bottom"
anchor="#link1"
oc-aria-label="Mein Konto"
class="mein-konto-popover"
>
<div class="header oc-px-100 oc-pb-75">
<h2 class="oc-headline-100">Mein Konto</h2>
<oc-icon-button-v3 icon="close" class="close"></oc-icon-button-v3>
</div>
<div class="oc-background-color-frame" style="min-height: 200px"></div>
<div class="actions oc-p-100 oc-gap-50 oc-mt-25">
<oc-button-v1 variant="primary">Anmelden</oc-button-v1>
<oc-button-v1 variant="secondary">Neu bei OTTO? Jetzt registrieren</oc-button-v1>
</oc-block-v2>
</oc-popover-v1>
</div>
<style>
.mein-konto-popover>.header {
padding-top: 20px;
display: flex;
justify-content: space-between;
}
.mein-konto-popover>.actions {
display: flex;
flex-direction: column;
}
</style>
`;
}
}
Interaction tests (PopoverV1.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Open and close popover with mouse
Story: components-popover-interaction-tests--open-and-close-with-mouse · tags: play-fn
Args: defaultSlot=Inhalt des Popover‚
Story source (TypeScript, verbatim from Storybook)
{
name: "Open and close popover with mouse",
args: {
defaultSlot: "Inhalt des Popover‚"
},
play: async ({
canvasElement
}) => {
const popoverContainer = canvasElement.getElementsByTagName("oc-popover-v1")[0];
const activator = canvasElement.getElementsByTagName("oc-link-v2")[0]!.shadowRoot!.querySelector("span")!;
// Not yet rendered
let toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
await expect(toggletip).toBeFalsy();
await userEvent.click(activator);
waitFor(() => {
toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
return expect(toggletip).not.toBeFalsy();
});
waitFor(() => {
toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
return expect(toggletip).toHaveClass("visible");
});
await userEvent.click(activator!);
await new Promise(res => {
setTimeout(res, 500);
});
waitFor(() => {
toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
// Should be removed from DOM
return expect(toggletip).toBeFalsy();
});
}
}
Open and close popover with keyboard
Story: components-popover-interaction-tests--open-and-close-with-keyboard · tags: skip-test, play-fn
Args: defaultSlot=Inhalt des Popover
Story source (TypeScript, verbatim from Storybook)
{
name: "Open and close popover with keyboard",
args: {
defaultSlot: "Inhalt des Popover"
},
tags: ["skip-test"],
// Skipped due to flakiness in both CI and local runs
play: async ({
canvasElement
}) => {
const popoverContainer = canvasElement.getElementsByTagName("oc-popover-v1")[0];
const activator = canvasElement.getElementsByTagName("oc-link-v2")[0];
console.log("activator", activator);
// Not yet rendered
let toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
await expect(toggletip).toBeFalsy();
focusDeepWithin(activator);
await userEvent.keyboard("{Enter}");
waitFor(() => {
toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
return expect(toggletip).toHaveClass("visible");
});
await userEvent.keyboard("{Escape}");
// Need longer then a tick, because we have some transitions and state updates before hiding
await new Promise(res => {
setTimeout(res, 500);
});
toggletip = popoverContainer.shadowRoot?.querySelector(".popover-popover");
await expect(toggletip).toBeFalsy();
await waitFor(async () => [expect(activator?.shadowRoot?.activeElement?.tagName).toEqual("SPAN") // link button should be focused
]);
}
}