Blueprint

Popover

A Popover is essentially an interactive Tooltip for displaying an overlay anchored to a particular element. It can set up to open automatically using a trigger element that also serves as its anchor point, or it can be opened manually positioned relative to an anchor element.

React


import { Popover } from '@hover/blueprint';

Basic usage

It is used to display contextual information to the user, and should be paired with a clickable trigger element.

When Popover opens, focus is sent to the popover content. When it closes, focus is returned to the trigger.

Note

When using this component, ensure the children passed to PopoverTrigger is focusable and has a forwarded ref.

live

<Popover
header="Confirmation!"
trigger={<Button shape="box">Trigger</Button>}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>

Close button

Pass isClosable to enable the built-in close button.

live

<Popover
isClosable
header="Confirmation!"
trigger={<Button shape="box">Trigger</Button>}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>

Hover

Pass isHover to open the popover when the trigger is hovered instead of clicked.

live

<Popover
isHover
header="Confirmation!"
trigger={<Button shape="box">Trigger</Button>}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>

Dark color scheme

live

<Popover
colorScheme="dark"
header="Confirmation!"
trigger={<Button shape="box">Trigger</Button>}
footer={
<HStack mt="100">
<Button color="light">Yes!</Button>
</HStack>
}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>

Rendering the Popover in a Portal

To render a popover in a Portal, pass the inPortal prop.

Info

These live code blocks hide overflow, so we're setting inPortal on all the examples

live

<Popover
inPortal
header="Header"
footer="This is the footer"
trigger={<Button shape="box">Trigger</Button>}
>
<Button shape="box">Button</Button>
</Popover>

Initial focus

By default, focus is to sent to the popover content when it opens. Pass the initialFocusRef prop to send focus to a specific element instead.

Note

Focusing a child element only works for popovers with a click trigger, not when isHover is set.

live

() => {
const initialFocusRef = React.useRef();
return (
<Popover
initialFocusRef={initialFocusRef}
placement="bottom"
trigger={<Button shape="box">Trigger</Button>}
closeOnBlur={false}
header="Manage Your Channels"
width="750"
footer={
<HStack justifyContent="space-between" mt="300">
<Body size={400} color="neutral.600">
Step 2 of 4
</Body>
<HStack>
<Button shape="box" size="medium" fill="outline">
Setup Email
</Button>
<Button shape="box" size="medium" ref={initialFocusRef}>
Next
</Button>
</HStack>
</HStack>
}
inPortal
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>
);
};

Controlled usage

You can control the opening and closing of the popover by passing the isOpen, and onClose props.

Sometimes you might need to set the returnFocusOnClose prop to false to prevent popover from returning focus to PopoverTrigger's children.

live

() => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<VStack alignItems="stretch" width="fit-content">
<Button shape="box" onClick={onToggle}>
External Trigger
</Button>
<Popover
returnFocusOnClose={false}
isOpen={isOpen}
onClose={onClose}
placement="right"
closeOnBlur={false}
trigger={
<Button color="warning" shape="box">
Internal Trigger
</Button>
}
header="Confirmation"
footer={
<HStack mt="100">
<Button fill="outline" shape="box" size="medium" onClick={onClose}>
Cancel
</Button>
<Button color="danger" shape="box" size="medium">
Apply
</Button>
</HStack>
}
inPortal
>
Are you sure you want to continue with your action?
</Popover>
</VStack>
);
};

Anchor

Pass anchor instead of trigger to prevent trigger any action. The wrapped component will become a position reference.

When passing an anchor instead of a trigger, the state of the Popover must be controlled with external state (controlled mode).

Anchor must accept a ref

As with trigger, if you pass a custom component inside anchor ensure that it uses forwardRef so the popover is triggered successfully.

live

() => {
const [isEditing, setIsEditing] = useBoolean();
const [color, setColor] = React.useState('salmon');
return (
<HStack>
<Popover
isOpen={isEditing}
onOpen={setIsEditing.on}
onClose={setIsEditing.off}
closeOnBlur={false}
isLazy
lazyBehavior="keepMounted"
anchor={
<TextInput
color={color}
w="auto"
display="inline-flex"
isDisabled={!isEditing}
size="small"
defaultValue="Popover Anchor"
/>
}
inPortal
width="fit-content"
bodyProps={{ display: 'flex', flexDirection: 'row' }}
>
<Label htmlFor="#colors" isSemiBold mr="300">
Colors:
</Label>
<RadioGroup
value={color}
onChange={newColor => setColor(newColor)}
id="colors"
>
<HStack>
<Radio value="salmon">salmon</Radio>
<Radio value="royalblue">royalblue</Radio>
<Radio value="turquoise">turquoise</Radio>
<Radio value="violet">violet</Radio>
</HStack>
</RadioGroup>
</Popover>
<Button onClick={setIsEditing.toggle} shape="box">
{isEditing ? 'Save' : 'Edit'}
</Button>
</HStack>
);
};

Accessing internal state

All Popover elements have access to the internal popover state (isOpen and onClose). Use the render prop pattern to gain access to them.

live

() => {
const initialRef = React.useRef();
return (
<Popover
closeOnBlur={false}
placement="left"
initialFocusRef={initialRef}
trigger={({ isOpen }) => (
<Button iconAfter={isOpen ? iArrowLeft : iArrowRight} shape="box">
Trigger
</Button>
)}
header={({ onClose }) => (
<>
This is the <em>header</em>,&nbsp;
<Link as="button" onClick={onClose}>
close me
</Link>.
</>
)}
footer={({ onClose }) => (
<>
This is the <em>footer</em>,&nbsp;
<Link as="button" onClick={onClose}>
close me
</Link>.
</>
)}
inPortal
>
{({ onClose }) => (
<Body mt="0" mb="200">
Hello. Nice to meet you! This is the <em>body</em> of the popover.
<Link as="button" onClick={onClose} ref={initialRef}>
Click here to close me.
</Link>
</Body>
)}
</Popover>
);
};

Placement

Since popover is powered by Popper, you can change the placement of the popover by passing the placement prop.

The possible values are:

StartCenterEnd
bottom-startbottom (default)bottom-end
auto-startautoauto-end
top-starttoptop-end
left-startleftleft-end
right-startrightright-end
Info

Even hen you've specified the placement manually, Popover will try to reposition itself in the event that available space at the specified placement isn't enough.

live

() => {
const [placement, setPlacement] = React.useState('top-start');
return (
<HStack>
<Spacer flex={1} />
<Popover
placement={placement}
header="Popover placement"
trigger={<Button shape="box">Click Me</Button>}
inPortal
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>
<Select
flex={1}
onChange={event => setPlacement(event.target.value)}
size="small"
value={placement}
>
{[
'bottom-start',
'bottom',
'bottom-end',
'auto-start',
'auto',
'auto-end',
'top-start',
'top',
'top-end',
'left-start',
'left',
'left-end',
'right-start',
'right',
'right-end',
].map(p => (
<option key={p} value={p}>
{p}
</option>
))}
;
</Select>
</HStack>
);
};

Lazily mounting Popover

By default, the Popover component renders its content to the DOM, meaning that invisible popover contents are still rendered but are hidden by styles.

If you want to defer rendering of popover content until that Popover is opened, you can use the isLazy prop. This is useful if your content needs to be extra performant, or make network calls on mount that should only happen when the component is displayed.

live

<Popover
isLazy
inPortal
header="Lazy Content"
trigger={<Button shape="box">Click Me</Button>}
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>

Accessibility

Info

When you see the word "trigger", it is referring to the children of PopoverTrigger

Keyboard and Focus

  • When the popover is opened, focus is moved to the PopoverContent. If the initialFocusRef is set, then focus moves to the element with that ref.
  • When the popover is closed, focus returns to the trigger. If you set returnFocusOnClose to false, focus will not return.
  • If trigger is set to hover:
    • Focusing on or mousing over the trigger will open the popover.
    • Blurring or mousing out of the trigger will close the popover. If you move your mouse into the PopoverContent, it'll remain visible.
  • If trigger is set to click:
    • Clicking the trigger or using the Space or Enter when focus is on the trigger will open the popover.
    • Clicking the trigger again will close the popover.
  • Hitting the Esc key while the popover is open and focus is within the PopoverContent, will close the popover. If you set closeOnEsc to false, it will not close.
  • Clicking outside or blurring out of the PopoverContent closes the popover. If you set closeOnBlur to false, it will not close.

ARIA Attributes

  • If the trigger is set to click, the PopoverContent element has role set to dialog. If the trigger is set to hover, the PopoverContent has role set to tooltip.
  • The PopoverContent has aria-labelledby set to the id of the PopoverHeader.
  • The PopoverContent has aria-describedby set to the id of the PopoverBody.
  • The PopoverContent has aria-hidden set to true or false depending on the open/closed state of the popover.
  • The trigger has aria-haspopup set to true to denote that it triggers a popover.
  • The trigger has aria-controls set to the id of the PopoverContent to associate the popover and the trigger.
  • The trigger has aria-expanded set to true or false depending on the open/closed state of the popover.