Headless Kit v0.2.2
DISCLAIMER: This component is in
Beta
status. That means that it is ready for production, but the API might change.

Popover

A non-modal primitive with overlays that open and close around a DOM element.

My Hero!

The Qwik UI Popover component is designed to align with the HTML Popover API Specification. As of now, native support for this API is around 72%. To ensure consistent behavior across all browsers, Qwik UI provides a polyfill for browsers that do not support the API natively.

Building blocks

import { component$, useStyles$ } from '@builder.io/qwik';
import { Popover, PopoverTrigger } from '@qwik-ui/headless';
 
export default component$(() => {
  return (
    <>
      <PopoverTrigger popovertarget="building-blocks">Inside Trigger</PopoverTrigger>
      <Popover id="building-blocks">Inside Popover</Popover>
    </>
  );
});

🎨 Anatomy

ComponentDescription
PopoverTrigger

An HTML Button component that opens the popover when clicked.

Popover

An HTML Element that is above other content on the page.

What is a Popover?

A popover is a non-modal UI element that creates overlays around a DOM element. Non-modal meaning the rest of the page can be interacted with while the popover is shown.

This is particularly useful for displaying additional information or options without navigating away from the current context. It is also a native replacement for portals, which are commonly used in other JavaScript frameworks.

I'm in the :top-layer pseudo element on supported browsers like chrome.

On unsupported browsers I'm in the qwik-ui-polyfill div at the end of the document.

Most importantly, this API is a foundation for other headless components. Qwik UI is one of the first (if not the first) headless libraries to align with the specification. The content inside of Qwik UI popovers are also natively resumable.

Even if a popover is in the HTML Tree, its children will not execute unless resumed.

Versatile use cases WIP

Here are a couple of example components where the Popover API can be used. Including other parts of Qwik UI headless. Throughout the documentation, we will show the Combobox being used as an example.

ComponentDescription
Combobox

A text input that allows users to fill in values from a predefined list.

Select

A dropdown menu that allows users to choose one value from a list.

Context Menu

A menu that appears upon user interaction, such as right-clicking.

Tooltip

A text label that appears when a user hovers, focuses, or touches an element.

Toast

A small message that shows up temporarily to give the user some feedback.

Dropdown Menu

A list of options that appears below a button, that users can select from.

Hover Card

A card that appears when a user hovers over an element.

Menubar

A horizontal menu with clickable items.

Caveats

Psuedo Selectors

:popover-open

In the context of a native popover, the :popover-open pseudo selector is used when the popover is open. However, pseudo selectors can't be polyfilled within CSS.

As a result, the polyfill adds the .popover-open CSS class. This means an open popover on unsupported browsers will have class=":popover-open" in the DOM.

A separate declaration is needed to select popovers in all browsers.

/* unsupported */
.\popover-open {
  background: red;
}
 
/* supported */
:popover-open {
  background: red;
}

Cross-browser Animations

Entry and exit animations have often been a frustrating experience in web development. Especially trying to animate between display: none, a discrete property.

Until recently, discrete properties were not animatable. Luckily, there's new properties to CSS that solve this problem.

The browser support for these is similar to support of the popover API. That said, the Qwik UI team has done an awesome job of managing animations on polyfill browsers for you using the popover-showing and popover-closing classes.

.my-transition {
  transition: opacity 0.5s, display 0.5s, overlay 0.5s;
 
  /* on new-line so the declaration is valid in all browsers */
  transition-behavior: allow-discrete;
 
  /* starting style for all browsers */
  opacity: 0;
}
 
.popover-showing {
  opacity: 1;
}
 
.popover-closing {
  opacity: 0;
}

Above is an example of a transition that works across browsers. The allow-discrete property allows us to transition both display and overlay. Overlay is a property that allows us to transition top-layer behavior.

Popovertarget

To add a popover trigger, it can be done similar to the native API, using the popovertarget attribute along with the corresponding popover id.

// Opens when trigger is clicked, with a matching id to popovertarget
<PopoverTrigger popovertarget="get-20-off">Get 20% off your next order!</PopoverTrigger>
 
// Native API:  popovertarget is an invoker
<button popovertarget="get-20-off">Get 20% off your next order!</button>

The popover attribute

A popover can be set to either auto (the default setting) or manual. What we've seen so far have been auto popovers.

Qwik UI adds the popover attribute for you under the hood.

// Qwik UI
<Popover id="my-id">"Auto vs Manual Popovers: A Qwik Guide to Playing Hide and Seek!"</Popover>
 
// Native API:  Adding popover here is the same as popover="auto"
<div id="my-id" popover>"Auto vs Manual Popovers: A Qwik Guide to Playing Hide and Seek!"</div>

Auto

Popover 1
Popover 2

An auto popover will automatically hide when you click outside of it and typically only one can be shown at a time.

Manual

Popover 1
Popover 2

On the other hand, a manual popover needs to be manually hidden, such as toggling the button or programmatically, and allows for scenarios like nested popovers in menus.

We can add a manual popover by adding the popover="manual" prop, or manual shorthand.

Programmatic Behavior

I was programmatically opened!

We can also enable programmatic behavior with popovers. Qwik UI provides several functions you can use to control this behavior.

PropTypeDescription
showPopover()
QRL

Opens the popover.

hidePopover()
QRL

Closes the popover.

togglePopover()
QRL

Toggles the popover between the open and closed state.

We can access them anywhere by importing the usePopover hook. We also need to pass in the popover id or popovertarget string to usePopover, which will find the popover we intend to perform an action on.

For example, here is programmatic behavior syncing the popover state with the listbox in Qwik UI's Combobox component.

// near the top of the component
const { showPopover, hidePopover } = usePopover(popoverId);
 
useTask$(async ({ track }) => {
  track(() => context.isListboxOpenSig.value);
 
  if (isServer) return;
 
  if (context.isListboxOpenSig.value) {
    showPopover();
  } else {
    hidePopover();
  }
});

Floating Behavior

To use the popover API with floating elements, you can add the floating={true} prop to the Popover component. This API enables the use of JavaScript to dynamically position the listbox using the top & left absolute properties.

To float an element, it must have an anchored element to latch onto. This can be done with the anchorRef prop.

Below is a mini tooltip implementation enabled by anchor behavior. Keep in mind, this is not accessible, but an example of how this API can be used. We strongly suggest using the Qwik UI Tooltip component.

AnchorRef Prop

I'm a mini tooltip!

Floating Prop

This API is purposely opt-in / incremental. At some point JavaScript will not be needed here at all, and so we want to ensure a smooth migration path when that becomes widely supported.

We chose not to use an Anchor polyfill here due to the difference in bundle size compared to a JavaScript implementation.

Configuring the Listbox

The Popover component is designed for positioning elements that float and facilitating interactions with them.

For the following examples, we'll be using the Combobox component. ComboboxPopover is merely a wrapper of the Popover component. Under the hood, it looks something like this:

<Popover
  {...props}
  id={popoverId}
  ref={context.popoverRef}
  manual
  floating={true}
  // "stick" to the input
  anchorRef={context.inputRef}
  class="listbox"
>
  <Slot />
</Popover>

Placement

To set the default position of the listbox, you can use the placement prop. In the example below, we've set placement to top. When the user opens the listbox, it will be above the input.

Flip

Allows the listbox to flip its position based on available space. It's enabled by default, but can be disabled by adding flip={false} on the listbox.

☝️ Scroll up and down with me open! 👇

Gutter

In the previous docs examples, we use the gutter property on the listbox. Gutter is the space between the anchor element and the floating element.

Styling

Styles can be added normally like any other component in Qwik UI, such as adding a class. The Popover API however, exposes the [popover] and :popover-open attribute and pseudo-class selectors which can be used to style both the open and closed states.

From an earlier section, we learned that the :popover-open psuedo-class cannot be polyfilled, and so a class is added instead.

[popover] {
/* Make the popover a grid */
display: grid;
}
 
[popover]:not(:popover-open) {
  /* Make sure to hide it unless open */
  display: none;
}
 
/* Duplicate for polyfill browsers */
[popover]:not(.\:popover-open) {
  display: none;
}

If Tailwind is the framework of choice, then styles can be added using the arbitrary variant syntax or @apply command. Below is an example of styling with [popover] as an arbitrary variant.

Popover

Listbox preset

By default, the popover API comes with built-in styles, including fixed behavior, margin, the list goes on.

There are times when we want to override this behavior. An example being when we want an absolutely positioned listbox.

To do that, we can add the listbox class to the popover component.

If you need to override any of the listbox properties, use the following CSS variables:

PropertyDescription
margin

--listbox-margin

padding

--listbox-padding

border

--listbox-border

overflow

--listbox-overflow

position

--listbox-position

Animations

To use an animation, add the following CSS classes to the component.

Here's the CSS imported from the example:

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
 
@keyframes fadeOut {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
 
.popover-showing {
  animation: fadeIn both 500ms ease;
}
 
.popover-closing {
  animation: fadeOut both 500ms ease;
}

Transition declarations

Transitions use the same classes for entry and exit animations. Those being .popover-showing and .popover-closing. They are explained more in the Caveats section.

Additional References

Qwik UI aims to be in line with the standard whenever possible. Our goal is to give Qwik developers the proper tooling when it comes to creating accessible & complex web applications.

To read more about the popover API you can check it out on:

Backdrops

Supported browsers provide a ::backdrop pseudo element. For those looking to add a backdrop, the modal component might be a better option, as it provides more browser support for backdrops.

A backdrop across all browsers in the popover is possible too, but it requires a bit of extra work on the consumer side.

/* I style this as if it is the backdrop */
<Popover id="my-backdrop">
  <div class="wrap">
    <div class="content">
      <Slot />
    </div>
  </div>
</Popover>

That said, if browser support is not a priority, feel free to use the native ::backdrop. They are still supported in every major browser.

Popover

PropTypeDescription
id
string

Popover's id. Should match the popover target.

popover
union

Defines the popover behavior, can be auto or manual. Default is auto.

manual
boolean

A manual popover needs to be manually hidden, such as toggling the button or programmatically.

floating
boolean

Enables extra JavaScript behavior for floating elements.

anchorRef
Signal

Signal reference that can be passed for floating behavior.

flip
boolean

Flips the placement of the popover when it starts to collide with the boundaries.

gutter
number

The space between the floating element and the anchored element.

placement
union

Flips the placement of the popover when it starts to collide with the boundaries.

autoPlacement
boolean

Automatically places the listbox based on available space.

[popover]
selector

Selects the popover on all browsers.

:popover-open
selector

Native supported pseudo element when the popover is open.

.popover-open
class

Polyfill class added to style unsupported browsers.

.popover-showing
class

Class to animate entry behavior.

.popover-closing
class

Class to animate close behavior.

listbox
class

Class to add to the popover component for listbox behavior.

Popover Trigger

PropTypeDescription
popovertarget
union

Accepts a string that matches the id of the popover.

usePopover hook

PropTypeDescription
showPopover()
QRL

Opens the popover.

hidePopover()
QRL

Closes the popover.

togglePopover()
QRL

Toggles the popover between the open and closed state.

Additional Examples

Auto Placement

Automatically places the listbox based on available space. You must set flip to false before using it. This comes in handy when you're unsure about the optimal placement for the floating element, or if you prefer not to set it manually.

My Car Collection 🚘