OTTODesign System

Code

Popover

Storybook group: Components · Sidebar path: Components/Popover · Extracted 28.09.2026

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-v2 over the tooltip variant of the popover. Only use the tooltip variant of the popover when you need popover-specific features such as custom content, sticky behavior, or a longpress/click trigger.

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: mouseenter and mouseleave are only supported for pointer devices and not for keyboard navigation or touch devices. longpress is 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: () => void

    Trigger 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: void

    Example:

    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
    ]);
  }
}