Popover
A popup that goes above other content on the page. You can still interact with the rest of the page while the popover is open.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root gutter={4}>
<Popover.Trigger class="popover-trigger">Click me</Popover.Trigger>
<Popover.Panel class="popover-panel">
I am anchored to the popover trigger!
</Popover.Panel>
</Popover.Root>
);
});
- In the Top Layer (above the rest of the page)
- Built on top of the native popover API specification
- Polyfill for unsupported browsers
- Executes code on interaction
- Float and position the popover
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root>
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>
<Popover.Panel class="popover-panel">My Hero!</Popover.Panel>
</Popover.Root>
);
});
The Qwik UI Popover component is built on top of the Native Popover API. Support is in every browser, and will increase over time.
While we wait for support though, Qwik UI uses a polyfill under the hood for you, so that you can use the component in all browsers.
Building blocks
🎨 Anatomy
Component | Description |
Popover.Root | The parent container for the popover trigger and panel. |
Popover.Trigger | A button that opens the popover when interacted with. |
Popover.Panel | 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.
It's great for displaying additional information or options without navigating away from the current context.
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.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root>
<Popover.Trigger class="popover-trigger">Inspect the popover!</Popover.Trigger>
<Popover.Panel class="popover-panel">
<p>
I'm in the <strong>:top-layer</strong> pseudo element on supported browsers like
chrome.
</p>
<p style={{ marginTop: '1rem' }}>
On unsupported browsers I'm in the qwik-ui-polyfill div at the end of the
document.
</p>
</Popover.Panel>
</Popover.Root>
);
});
When would I use a popover?
It can help prevent overflow issues within your UI, and make sure your content is above everything. The popover component guarantees that your items in the panel remains above the rest of the page.
Use case examples
Component | Description |
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. |
Caveats
Styling open popovers
Use the data-open
attribute on the <Popover.Panel>
component to specifically style the popup when it's open on all browsers.
Popover modes
Auto
By default, a popover is auto.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<>
{[0, 1].map((i) => (
<Popover.Root key={i}>
<Popover.Trigger class="popover-trigger">
Popover Trigger {i + 1}
</Popover.Trigger>
<Popover.Panel style={{ top: i === 1 ? '25px' : '0' }} class="popover-panel">
Popover {i + 1}
</Popover.Panel>
</Popover.Root>
))}
</>
);
});
An auto popover will automatically hide when you click outside of it and typically only one can be shown at a time.
Auto popovers dismiss when clicking outside of them and pressing the escape key. There can also only be one auto popover open at a time.
Manual
A manual popover gives you more control over the popup.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<>
{[0, 1].map((i) => (
<Popover.Root manual key={i}>
<Popover.Trigger popovertarget={`manual-${i}`} class="popover-trigger">
Popover Trigger {i + 1}
</Popover.Trigger>
<Popover.Panel style={{ top: i === 1 ? '25px' : '0' }} class="popover-panel">
Popover {i + 1}
</Popover.Panel>
</Popover.Root>
))}
</>
);
});
Manual popovers do not dismiss unless you click on the <Popover.Trigger>
or programmatically close them.
Programmatic Behavior
To programmatically open the Popover.Panel
, Qwik UI provides a couple functions from the usePopover
hook.
Prop | Type | Description |
---|---|---|
showPopover() | QRL | Opens the popover. |
hidePopover() | QRL | Closes the popover. |
togglePopover() | QRL | Toggles the popover between the open and closed. |
import { component$, useSignal } from '@builder.io/qwik';
import { Popover, usePopover } from '@qwik-ui/headless';
export default component$(() => {
const popoverId = 'programmatic-id';
const anchorRef = useSignal<HTMLElement | undefined>();
const { togglePopover } = usePopover(popoverId);
return (
<Popover.Root id={popoverId} bind:anchor={anchorRef} manual>
{/* can be anywhere as long as ref is set */}
<button
ref={anchorRef}
class="popover-invoker"
preventdefault:click
onKeyDown$={async (e) => {
if (e.key === 'o') {
await togglePopover();
}
}}
onClick$={async () => {
await togglePopover();
}}
>
Click me or focus me and press the 'o' key!
</button>
<Popover.Panel class="popover-panel popover-programmatic">
I'm a programmatically opened popover!
</Popover.Panel>
</Popover.Root>
);
});
We can control where the popover is floating by using the bind:anchor
prop. We pass it a signal, and use that signal as the anchor's ref
.
Then, make sure to add an ID to the <Popover.Root>
component. This ID must match the ID provided to usePopover
.
Above is an example of how we programmatically open and close the popover in the Qwik UI Select component.
Handling state changes
Panel is closed!
import { component$, useSignal } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
const toggleStatus = useSignal<'open' | 'closed'>('closed');
return (
<Popover.Root>
<p>Panel is {toggleStatus.value}!</p>
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>
<Popover.Panel
class="popover-panel"
onToggle$={(e) => {
toggleStatus.value = e.newState;
}}
>
My Hero!
</Popover.Panel>
</Popover.Root>
);
});
Use the onToggle$
prop on the <Popover.Panel>
to listen for changes to the popover visibility.
The example above uses a Signal
to track whether the popover is triggered or not.
Floating Behavior
By default, the Qwik UI Popover will float below the trigger component.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root gutter={4}>
<Popover.Trigger class="popover-trigger">Click me</Popover.Trigger>
<Popover.Panel class="popover-panel">I am anchored to the trigger!</Popover.Panel>
</Popover.Root>
);
});
To make a popover float, we use JavaScript to choose where the popover should be positioned.
Floating mode makes the bundle a bit bigger. We try to keep the API small but still powerful.
Opt out of the floating library
We can opt out of the floating library by setting floating={false}
on the <Popover.Root>
.
Instead, the popover will be fixed position, and you can use CSS to position it.
Configuration
The Popover.Root
component is designed for positioning elements that float and facilitating interactions with them.
Custom Floating Position
By default, popovers will float below the trigger component.
When setting floating
on the root, you can customize the position of the popover.
popover on the right ⤵️
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root gutter={4} floating="right">
<div class="popover-container">
<p>popover on the right ⤵️</p>
<Popover.Trigger class="popover-trigger">Click me</Popover.Trigger>
</div>
<Popover.Panel class="popover-panel">I am anchored to the trigger!</Popover.Panel>
</Popover.Root>
);
});
Above we have set the floating
prop to right
, and so the <Popover.Panel>
will be positioned to the right of the trigger.
Start & End
This also includes moving things to specific corners of the anchor.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root gutter={4} floating="top-end">
<div class="popover-container">
<Popover.Trigger class="popover-trigger">Click me</Popover.Trigger>
</div>
<Popover.Panel class="popover-panel">I am on the top-right corner!</Popover.Panel>
</Popover.Root>
);
});
You can suffix the direction with -start
or -end
to move the popover to the start or end of the anchor.
Flip
Enabled by default, we can use the flip
prop to flip its position based on the available space in the viewport.
auto placed on scroll 📜
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root gutter={4}>
<div class="popover-container">
<p>auto placed on scroll 📜</p>
<Popover.Trigger class="popover-trigger">Click me</Popover.Trigger>
</div>
<Popover.Panel class="popover-panel">I am anchored to the trigger!</Popover.Panel>
</Popover.Root>
);
});
To disable flipping, set flip={false}
on the <Popover.Root>
.
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.
gutter of 40px!
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root floating="top" gutter={40}>
<div class="popover-container">
<p>gutter of 40px!</p>
<Popover.Trigger popovertarget="gutter-id" class="popover-trigger">
Click me
</Popover.Trigger>
</div>
<Popover.Panel class="popover-panel">I am anchored to the trigger!</Popover.Panel>
</Popover.Root>
);
});
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]
attribute.
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.
import { component$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
return (
<Popover.Root>
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>
{/* popover background-color gets overrided */}
<Popover.Panel class="[&[popover]]: [&[popover]]:border-accent [&[popover]]:bg-primary [&[popover]]:p-3 [&[popover]]:text-foreground">
Popover
</Popover.Panel>
</Popover.Root>
);
});
Floating preset
By default, the popover API comes with built-in browser 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 floating behavior.
Qwik UI strips the following styles when in floating mode:
/* Strips the user agent styles from the popover when in floating mode */
@layer qwik-ui {
[data-floating] {
margin: unset;
padding: unset;
border: unset;
overflow: unset;
position: absolute;
}
}
/** override the polyfill's layer, which gets dynamically imported later on. */
@layer popover-polyfill {
[data-floating] {
margin: unset;
padding: unset;
border: unset;
overflow: unset;
position: absolute;
}
}
We put it under an @layer
so that it can be easily overridden when adding your own styles.
Animations
Popovers present unique challenges for animations due to their reliance on the display
property, which traditionally hasn't been animatable. Modern browsers solve this problem with discrete animations, which allow smooth transitions between display: none
and display: block
. This ensures that popovers remain visible for the entire duration of the animation.
There are two main types of animations that can be applied to popovers in Qwik UI:
- Keyframe Animations: For more complex entry and exit animations, such as growing or shrinking effects.
- CSS Transitions: For smoother, incremental property changes like opacity and scaling.
To handle animations across all browsers, Qwik UI uses both keyframes and transitions, leveraging the native :popover-open
pseudo-class and ensuring smooth state transitions with transition-behavior: allow-discrete
.
Keyframe Animation Example
Keyframes are ideal for handling the entry and exit of the popover. Below is an example using popover-grow
for opening and popover-shrink
for closing the popover:
import { component$, useStyles$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(`
.popover-animation {
animation: popover-shrink 0.4s ease-in-out forwards;
}
/* For exit animation */
.popover-animation:popover-open {
animation: popover-grow 0.5s ease-in-out forwards;
}
@keyframes popover-shrink {
from {
transform: scale(1);
display: block;
}
to {
transform: scale(0);
display: none;
}
}
@keyframes popover-grow {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
`);
return (
<Popover.Root>
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>
<Popover.Panel class="popover-panel popover-animation">
I'm a popover!
</Popover.Panel>
</Popover.Root>
);
});
Transition declarations
CSS transitions are useful for animating properties like opacity and scale over time. Discrete properties like display and overlay can be handled using transition-behavior: allow-discrete.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Popover } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(`
.popover-transition {
opacity: 0;
transform: scale(0.5);
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out,
display 0.3s,
overlay 0.3s;
transition-behavior: allow-discrete;
}
.popover-transition:popover-open {
opacity: 1;
transform: scale(1);
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out,
display 0.3s,
overlay 0.3s;
}
@starting-style {
.popover-transition:popover-open {
opacity: 0;
transform: scale(0.5);
}
}`);
return (
<Popover.Root>
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>
<Popover.Panel class="popover-panel popover-transition">
I'm a popover!
</Popover.Panel>
</Popover.Root>
);
});
By using keyframe animations and CSS transitions together, we can achieve smooth entry and exit effects for popovers. Keyframes like popover-grow
and popover-shrink
handle scaling and visibility, while transitions focus on gradual changes to properties like opacity
. Remember to include display
and overlay
in your transition properties and use transition-behavior: allow-discrete
to ensure smooth animations across all browsers.
Additional References
Qwik UI aims to be in line with the standard whenever possible. Our goal is to empower Qwik developers to create amazing experiences for their users.
To read more about the popover API you can check it out on:
Popover Root
Prop | Type | Description |
---|---|---|
id | string | Popover's id. Should match the popover target. |
manual | boolean | A manual popover needs to be manually hidden, such as toggling the button or programmatically. |
floating | boolean | TPlacement | 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. |
[popover] | selector | Selects the popover on all browsers. |
data-open | selector | Style the element when the popover is open. |
data-closed | selector | Style the element when the popover is closed. |
Popover Panel
Prop | Type | Description |
---|---|---|
onToggle | QRL QRL<(event: CorrectedToggleEvent, element: HTMLDivElement) => void> | Function called when the popover opens or closes |
usePopover hook
Prop | Type | Description |
---|---|---|
showPopover() | QRL | Opens the popover. |
hidePopover() | QRL | Closes the popover. |
togglePopover() | QRL | Toggles the popover between the open and closed state. |