Overlays
Popover
Display a non-modal dialog that floats around a trigger.
Import
tsx
import { Popover } from "@hope-ui/core";
tsx
import { Popover } from "@hope-ui/core";
- Popover: The wrapper that provides props, state, and context to its children.
- Popover.Trigger: The component that opens/closes the popover.
- Popover.Anchor: The component to use as positioning reference instead of the trigger.
- Popover.Content: The popover itself.
- Popover.CloseButton: A button to close the popover.
- Popover.Heading: An optional heading for the popover.
- Popover.Description: An optional description for the popover.
Usage
Popover.Trigger
renders an unstyled button
by default. For accessibility reason, when using the as
prop ensure the element passed in is focusable.
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Trigger mode
Use the triggerMode
prop to change the way of opening the popover. You can set the value to click
(default) or hover
.
tsx
<Popover triggerMode="hover"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover triggerMode="hover"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Initial focus
When the popover opens, focus is automatically set on the first focusable element in Popover.Content
.
Use the data-autofocus
attribute on any element to mark it as the initial focus element.
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button _focus={{ color: "red" }}>Button 1 </Button><Button data-autofocus _focus={{ color: "red" }}>Button 2</Button></HStack></Popover.Content></Popover>
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button _focus={{ color: "red" }}>Button 1 </Button><Button data-autofocus _focus={{ color: "red" }}>Button 2</Button></HStack></Popover.Content></Popover>
You can also use a different css selector by using the initialFocusSelector
prop.
tsx
<Popover initialFocusSelector="#initial-focus"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button _focus={{ color: "red" }}>Button 1 </Button><Button id="initial-focus" _focus={{ color: "red" }}>Button 2</Button></HStack></Popover.Content></Popover>
tsx
<Popover initialFocusSelector="#initial-focus"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button _focus={{ color: "red" }}>Button 1 </Button><Button id="initial-focus" _focus={{ color: "red" }}>Button 2</Button></HStack></Popover.Content></Popover>
If the popover contains no focusable element, focus is set to the Popover.Content
itself.
Restore focus
When the popover closes, focus is set back to the trigger element. Use the restoreFocusSelector
prop and pass in a css selector to target another element.
tsx
<HStack spacing={4}><Popover restoreFocusSelector="[data-finalfocus]"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover><Button data-finalfocus _focus={{ color: "red" }}>Final focus element</Button></HStack>
tsx
<HStack spacing={4}><Popover restoreFocusSelector="[data-finalfocus]"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover><Button data-finalfocus _focus={{ color: "red" }}>Final focus element</Button></HStack>
Focus trap
Use the trapFocus
prop to lock focus inside the Popover.Content
.
tsx
<Popover trapFocus><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button>Button 1</Button><Button>Button 2</Button></HStack></Popover.Content></Popover>
tsx
<Popover trapFocus><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><HStack spacing={4}><Button>Button 1</Button><Button>Button 2</Button></HStack></Popover.Content></Popover>
Heading and description
Use Popover.Heading
to add an accessible heading to the popover, it renders an h2
by default.
Similarly Popover.Description
is used add an accessible description and renders a p
by default.
Under the hood, Hope UI will set the correct aria-labelledby
and aria-describedby
attributes.
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><Popover.Heading fontWeight="semibold">Title</Popover.Heading><Popover.Description fontSize="sm" color={{ light: "neutral.600", dark: "neutral.300" }} mb={4}>Description</Popover.Description><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><Popover.Heading fontWeight="semibold">Title</Popover.Heading><Popover.Description fontSize="sm" color={{ light: "neutral.600", dark: "neutral.300" }} mb={4}>Description</Popover.Description><p>The content of the Popover.</p></Popover.Content></Popover>
Same width
Use hasSameWidth
prop to make the popover take the same width as the trigger element.
tsx
<Popover hasSameWidth><Popover.Trigger as={Button} w={96}>Trigger</Popover.Trigger><Popover.Content p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover hasSameWidth><Popover.Trigger as={Button} w={96}>Trigger</Popover.Trigger><Popover.Content p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Arrow
Use the arrowSize
prop to change the popover's arrow size and the offset
prop to adjust the distance with the trigger.
tsx
<Popover arrowSize={30} offset={15}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover arrowSize={30} offset={15}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
You can also remove the arrow by setting the withArrow
prop to false
.
tsx
<Popover withArrow={false} offset={8}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover withArrow={false} offset={8}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Placement
Use the placement
prop to set the preferred popover placement. You can set the value to top
, right
, left
, bottom
and their -start
or -end
variants.
tsx
<Popover placement="top-start"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover placement="top-start"><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Even though you specified the placement, Popover will try to reposition itself in the event that available space at the specified placement isn't enough.
Open/close transitions
The transitionOptions
prop allows to customize the popover open/close transitions. You can use predefined transitions or create custom ones.
tsx
<PopovertransitionOptions={{transition: "slide-up",duration: 400,exitDuration: 250,easing: "ease-out",exitEasing: "ease-in",}}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<PopovertransitionOptions={{transition: "slide-up",duration: 400,exitDuration: 250,easing: "ease-out",exitEasing: "ease-in",}}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
To learn more about the transition API check out the createTransition documentation.
Popover anchor
Use the Popover.Anchor
component if you want to use a different element for the popover positioning reference, it rendes a div
by default.
In the example below, the Button
is the trigger that opens/closes the popover and the Icon
is the reference element that the popover will position relative to.
tsx
<Popover><HStack spacing={4}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Anchor as={TargetIcon} boxSize={6} /></HStack><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
tsx
<Popover><HStack spacing={4}><Popover.Trigger as={Button}>Trigger</Popover.Trigger><Popover.Anchor as={TargetIcon} boxSize={6} /></HStack><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover>
Using virtual element
The getAnchorRect
prop allows to use a virtual element as popover's anchor.
The below example shows how to make a popover that follow user's cursor.
tsx
function FollowCursorExample() {const [anchorRect, setAnchorRect] = createSignal({ x: 0, y: 0 });const [isOpen, setIsOpen] = createSignal(false);return (<><Centerheight="100px"rounded="sm"borderWidth="2px"borderStyle="dashed"borderColor="neutral.300"onMouseEnter={() => setIsOpen(true)}onMouseLeave={() => setIsOpen(false)}onMouseMove={event => {event.preventDefault();setAnchorRect({ x: event.clientX, y: event.clientY });}}>Hover the area.</Center><Popover isOpen={isOpen()} onOpenChange={setIsOpen} getAnchorRect={anchorRect}><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></>);}
tsx
function FollowCursorExample() {const [anchorRect, setAnchorRect] = createSignal({ x: 0, y: 0 });const [isOpen, setIsOpen] = createSignal(false);return (<><Centerheight="100px"rounded="sm"borderWidth="2px"borderStyle="dashed"borderColor="neutral.300"onMouseEnter={() => setIsOpen(true)}onMouseLeave={() => setIsOpen(false)}onMouseMove={event => {event.preventDefault();setAnchorRect({ x: event.clientX, y: event.clientY });}}>Hover the area.</Center><Popover isOpen={isOpen()} onOpenChange={setIsOpen} getAnchorRect={anchorRect}><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></>);}
You can also pair it with the createInteractOutside
primitive to create a context menu like behavior.
tsx
function RightClickExample() {let containerRef: HTMLDivElement | undefined;const [anchorRect, setAnchorRect] = createSignal({ x: 0, y: 0 });const [isOpen, setIsOpen] = createSignal(false);createInteractOutside({onInteractOutside: () => setIsOpen(false),},() => containerRef);return (<><Centerref={containerRef}height="100px"rounded="sm"borderWidth="2px"borderStyle="dashed"borderColor="neutral.300"onContextMenu={event => {event.preventDefault();setAnchorRect({ x: event.clientX, y: event.clientY });setIsOpen(true);}}>Right click in the area.</Center><Popover isOpen={isOpen()} onOpenChange={setIsOpen} getAnchorRect={anchorRect}><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></>);}
tsx
function RightClickExample() {let containerRef: HTMLDivElement | undefined;const [anchorRect, setAnchorRect] = createSignal({ x: 0, y: 0 });const [isOpen, setIsOpen] = createSignal(false);createInteractOutside({onInteractOutside: () => setIsOpen(false),},() => containerRef);return (<><Centerref={containerRef}height="100px"rounded="sm"borderWidth="2px"borderStyle="dashed"borderColor="neutral.300"onContextMenu={event => {event.preventDefault();setAnchorRect({ x: event.clientX, y: event.clientY });setIsOpen(true);}}>Right click in the area.</Center><Popover isOpen={isOpen()} onOpenChange={setIsOpen} getAnchorRect={anchorRect}><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></>);}
Accessing internal state
Popover provides access to its internal isOpen
state and a close
method that you can access via a render prop.
tsx
<Popover>{({ isOpen, close }) => (<><Popover.Trigger as={Button}>Click to {isOpen() ? "close" : "open"}</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p><Button onClick={close}>Close</Button></Popover.Content></>)}</Popover>
tsx
<Popover>{({ isOpen, close }) => (<><Popover.Trigger as={Button}>Click to {isOpen() ? "close" : "open"}</Popover.Trigger><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p><Button onClick={close}>Close</Button></Popover.Content></>)}</Popover>
Controlled popover
Use the isOpen
and onOpenChange
props to control the popover state.
tsx
function ControlledPopoverExample() {const [isOpen, setIsOpen] = createSignal(false);return (<HStack spacing={4}><Button onClick={() => setIsOpen(prev => !prev)}>Click to {isOpen() ? "close" : "open"}</Button><Popover isOpen={isOpen()} onOpenChange={setIsOpen} closeOnBlur={false}><Popover.Anchor as={TargetIcon} boxSize={6} /><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></HStack>);}
tsx
function ControlledPopoverExample() {const [isOpen, setIsOpen] = createSignal(false);return (<HStack spacing={4}><Button onClick={() => setIsOpen(prev => !prev)}>Click to {isOpen() ? "close" : "open"}</Button><Popover isOpen={isOpen()} onOpenChange={setIsOpen} closeOnBlur={false}><Popover.Anchor as={TargetIcon} boxSize={6} /><Popover.Content w="max-content" p={4}><p>The content of the Popover.</p></Popover.Content></Popover></HStack>);}
If the trigger is outside of the popover (like the above example), you may need to disabled
closeOnBlur
to prevents weird open/close behavior.
Accessibility
ARIA roles and attributes
Popover.Trigger
has:
aria-haspopup
set todialog
to denote that it triggers a popover.aria-controls
set to theid
of thePopover.Content
.aria-expanded
synced with the open/closed state of the popover.aria-labelledby
set to theid
of thePopover.Heading
, if any.aria-describedby
set to theid
of thePopover.Description
, if any.
Popover.Content
has:
role
set todialog
, when thetriggerMode
ishover
, role is set totooltip
.
Keyboard support (closed popover)
- In
click
mode, clicking thePopover.Trigger
or pressing space or enter when focus is on the trigger will open the popover. - In
hover
mode, focusing on or mousing over thePopover.Trigger
will open the popover.
Keyboard support (opened popover)
- If the
initialFocusSelector
prop is set, focus is moved to the matching element, otherwise focus moves to thePopover.Content
. - In
click
mode, clicking thePopover.Trigger
closes the popover. - In
hover
mode, blurring or mousing out of thePopover.Trigger
will close the popover. If you move your mouse into thePopover.Content
, it'll remain visible. - When focus is within the
Popover.Content
andcloseOnEsc
prop istrue
, pressing esc closes the popover. - When
closeOnBur
prop istrue
, clicking outside or blurring out of thePopover.Content
closes the popover. - When the popover closes, focus returns to the
Popover.Trigger
unless another focusable element takes focus.
Theming
Use the PopoverTheme
type to override Popover
styles and default props when extending Hope UI theme.
tsx
import { extendTheme, PopoverTheme } from "@hope-ui/core";const theme = extendTheme({components: {Popover: {// ...overrides} as PopoverTheme,},});
tsx
import { extendTheme, PopoverTheme } from "@hope-ui/core";const theme = extendTheme({components: {Popover: {// ...overrides} as PopoverTheme,},});
Component parts
Name | Static selector | Description |
---|---|---|
root | hope-Popover-root | Popover content |
arrow | hope-Popover-arrow | Popover arrow |
heading | hope-Popover-heading | Popover heading |
description | hope-Popover-description | Popover description |
Props
Popover
Name | Type | Description | Default value |
---|---|---|---|
isOpen | boolean | Whether the popover should be shown (in controlled mode). | |
defaultIsOpen | boolean | Whether the popover should be initially shown (in uncontrolled mode). | |
onOpenChange | (isOpen: boolean) => void | A function that will be called when the isOpen state of the popover changes. | |
id | string | The id of the popover content. | |
children | JSX.Element | PopoverChildrenRenderProp | Children of the popover. | |
transitionOptions | TransitionOptions | Options passed to the popover transition. | |
triggerMode | "hover" | "click" | The interaction that triggers the popover. | "click" |
withArrow | boolean | Whether the popover should display an arrow inside it. | true |
arrowSize | number | The size of the arrow (in px). | 24 |
arrowPadding | number | The minimum padding between the arrow and the popover corner (in px). | 12 |
placement | FloatingPlacement | Placement of the popover. | "bottom" |
hasSameWidth | boolean | Whether the popover should have the same width as the trigger/anchor element. | |
offset | number | Offset between the popover and the anchor element (in px). | 12 |
overflowPadding | number | The minimum padding between the popover and the viewport edge (in px). | |
openDelay | number | Delay before showing the popover on hover (in ms). | 200 |
closeDelay | number | Delay before hiding the popover on mouse leave (in ms). | 200 |
closeOnBlur | boolean | Whether the popover should close when the user blur out it by clicking outside or tabbing out. | true |
closeOnEsc | boolean | Whether the popover should close when the user hit the Esc key. | true |
trapFocus | boolean | Whether the focus will be locked into the popover. | false |
initialFocusSelector | string | A query selector to retrieve the element that should receive focus once the popover opens. | |
restoreFocusSelector | string | A query selector to retrieve the element that should receive focus once the popover closes. |
Other components props
Popover.CloseButton
renders a CloseButton and supports all of its props.Popover.Trigger
,Popover.Anchor
,Popover.Content
,Popover.Heading
andPopover.Description
supports commons Hope UI props (style props,sx
andas
).