Popover

Popover is a non-modal dialog that floats around a trigger. It is used to display contextual information to the user, and should be paired with a clickable trigger element.

Popover is built on top of the Popper.js library, and composes the Popper component.

Import#

  • Popover: The wrapper that provides props, state, and context to its children.
  • PopoverTrigger: Used to wrap the reference (or trigger) element.
  • PopoverContent: The popover itself.
  • PopoverHeader: The header of the popover.
  • PopoverBody: The body of the popover.
  • PopoverArrow: A visual arrow that points to the reference (or trigger).
  • PopoverCloseButton: A button to close the popover.
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverBody,
PopoverFooter,
PopoverArrow,
PopoverCloseButton,
} from "@nature-ui/core";

Basic Usage#

When using this component, ensure the children passed to PopoverTrigger is focusable. Users can tab to it using their keyboard, and it can take a ref. It is critical for accessiblity.

<Popover>
<PopoverTrigger>
<Button className="bg-primary-500">Trigger</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Confirmation!</PopoverHeader>
<PopoverBody>Are you sure you want to have that milkshake?</PopoverBody>
</PopoverContent>
</Popover>

Rendering the Popover in a Portal#

By default, the Popover doesn't render in a Portal. To make them display in a portal, wrap the PopoverContent in a Portal

You might need to Inspect Element to see this in action. Notice that PopoverContent is rendered as a child of <body>

<Popover>
<PopoverTrigger>
<Button className="bg-primary-500">Trigger</Button>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverHeader>Header</PopoverHeader>
<PopoverCloseButton />
<PopoverBody>
<Button className="bg-blue-500">Button</Button>
</PopoverBody>
<PopoverFooter>This is the footer</PopoverFooter>
</PopoverContent>
</Portal>
</Popover>

Focus an element when Popover opens#

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

function WalkthroughPopover() {
const initialFocusRef = React.useRef();
return (
<Popover
initialFocusRef={initialFocusRef}
placement="bottom"
closeOnBlur={false}
>
<PopoverTrigger>
<Button className="bg-primary-500">Trigger</Button>
</PopoverTrigger>
<PopoverContent className="w-full px-3 bg-blue-800 text-white border-blue-800">
<PopoverArrow />
<PopoverHeader className="pt-4 font-bold border-none">
Manage Your Channels
</PopoverHeader>
<PopoverCloseButton />
<PopoverBody>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
eiusmod tempor incididunt ut labore et dolore.
</PopoverBody>
<PopoverFooter className="flex pb-4 border-none justify-between items-center">
<Box className="text-sm">Step 2 of 4</Box>
<Stack row spacing="1rem">
<Button size="sm" className="block" color="green-500">
Setup Email
</Button>
<Button size="sm" color="blue-500" ref={initialFocusRef}>
Next
</Button>
</Stack>
</PopoverFooter>
</PopoverContent>
</Popover>
);
}

Trapping Focus within Popover#

If the popover contains a form, you might need to trap focus within the popover and close it when the user fills the form and hits "save".

You can leverage react-focus-lock to trap focus within the PopoverContent.

// import FocusLock from "react-focus-lock"
// 1. Create a text input component
const TextInput = React.forwardRef((props, ref) => {
return (
<FormControl>
<FormLabel htmlFor={props.id}>{props.label}</FormLabel>
<Input ref={ref} id={props.id} {...props} />
</FormControl>
);
});
// 2. Create the form
const CustomForm = ({ firstFieldRef, onCancel }) => {
return (
<Stack col spacing="8px">
<TextInput
label="First name"
id="first-name"
ref={firstFieldRef}
defaultValue="John"
/>
<TextInput label="Last name" id="last-name" defaultValue="Smith" />
<Stack row className="mt-4">
<Button
size="sm"
className="border border-gray-600 text-gray-600"
onClick={onCancel}
>
Cancel
</Button>
<Button size="sm" isDisabled className="bg-purple-500">
Save
</Button>
</Stack>
</Stack>
);
};
// 3. Create the Popover
// Ensure you set `closeOnBlur` prop to false so it doesn't close on outside click
const PopoverForm = () => {
const { onOpen, onClose, isOpen } = useDisclosure();
const firstFieldRef = React.useRef(null);
return (
<>
<Box className="inline-block mr-4">John Smith</Box>
<Popover
isOpen={isOpen}
initialFocusRef={firstFieldRef}
onOpen={onOpen}
onClose={onClose}
placement="right"
closeOnBlur={false}
>
<PopoverTrigger>
<IconButton size="sm" icon={<EditIcon />} />
</PopoverTrigger>
<PopoverContent className="p-5">
<FocusLock returnFocus persistentFocus={false}>
<PopoverArrow />
<PopoverCloseButton />
<CustomForm firstFieldRef={firstFieldRef} onCancel={onClose} />
</FocusLock>
</PopoverContent>
</Popover>
</>
);
};
render(<PopoverForm />);

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 popver from returning focus to PopoverTrigger's children.

function ControlledUsage() {
const [isOpen, setIsOpen] = React.useState(false);
const open = () => setIsOpen(!isOpen);
const close = () => setIsOpen(false);
return (
<>
<Button className={"mr-5 bg-primary-500"} onClick={open}>
Trigger
</Button>
<Popover
returnFocusOnClose={false}
isOpen={isOpen}
onClose={close}
placement="right"
closeOnBlur={false}
>
<PopoverTrigger>
<Button className="bg-pink-500">Popover Target</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader className="font-semibold">Confirmation</PopoverHeader>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
Are you sure you want to continue with your action?
</PopoverBody>
<PopoverFooter className="justify-end flex">
<Stack row>
<Button
size="sm"
className="border border-gray-500 text-gray-500"
>
Cancel
</Button>
<Button size="sm" className="bg-red-500">
Apply
</Button>
</Stack>
</PopoverFooter>
</PopoverContent>
</Popover>
</>
);
}

Accessing Internal state#

Nature UI provides access to two internal details: isOpen and onClose. Use the render prop pattern to gain access to them.

function InternalStateEx() {
const initRef = React.useRef();
return (
<Popover closeOnBlur={false} placement="bottom" initialFocusRef={initRef}>
{({ isOpen, onClose }) => (
<>
<PopoverTrigger>
<Button className="bg-primary-500">
Click to {isOpen ? "close" : "open"}
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader>This is the header</PopoverHeader>
<PopoverCloseButton />
<PopoverBody>
<Box>
Hello. Nice to meet you! This is the body of the popover
</Box>
<Button
className="mt-4"
className="bg-purple-600"
onClick={onClose}
ref={initRef}
>
Close
</Button>
</PopoverBody>
<PopoverFooter>This is the footer</PopoverFooter>
</PopoverContent>
</>
)}
</Popover>
);
}

Customizing the Popover#

Nature UI exports all the components you need to customize the look and feel of the popover. You can change the background, arrow size, box shadow and so on.

<Popover>
<PopoverTrigger>
<Box
tabIndex="0"
className="p-5 w-32 bg-gray-300"
role="button"
aria-label="Some box"
children="Click"
/>
</PopoverTrigger>
<PopoverContent className="bg-orange-500 text-white">
<PopoverHeader className="font-semibold">Customization</PopoverHeader>
<PopoverArrow className="bg-pink-500" />
<PopoverCloseButton className="bg-purple-500" />
<PopoverBody>
Tadaa!! The arrow color and background color is customized. Check the
props for each component.
</PopoverBody>
</PopoverContent>
</Popover>

Popover Placements#

Since popover is powered by PopperJS, you can change the placement of the popover by passing the placement prop. See the props for the possible placement values.

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.

<Popover placement="top-start">
<PopoverTrigger>
<Button className="bg-primary-500">Click me</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader className="font-semibold">Top-Start placement</PopoverHeader>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</PopoverBody>
</PopoverContent>
</Popover>

Lazily mounting Popover#

By default, the Popover component renders children of PopoverContent 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 PopoverContent needs to be extra performant, or make network calls on mount that should only happen when the component is displayed.

<Popover isLazy>
<PopoverTrigger>
<Button className="bg-primary-500">Click me</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader className="font-semibold">Popover placement</PopoverHeader>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</PopoverBody>
</PopoverContent>
</Popover>

Accessiblity#

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 clickable elements in PopoverContent component. If the initialFocusRef is set, then focus moves to the element with that ref.
  • 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.

Props#

Popover Props#

NameTypeDefaultDescription
isOpenbooleanIf true, the popover is shown
defaultIsOpenbooleanIf true, the popover is shown initially.
initialFocusRefReact.RefThe ref of the element that should receive focus when the popover opens.
triggerhover, clickclickThe interaction that triggers the popover.
placementPopperJS.placementbottomThe placement of the popover.
returnFocusOnClosebooleantrueIf true, the popover will return focus to the trigger when it closes
closeOnBlurbooleantrueIf true, the popover will close when you blur out it by clicking outside or tabbing out
closeOnEscbooleantrueIf true, close the popover when the esc key is pressed
childrenReact.ReactNode, (props: InternalState) => React.ReactNodeThe children of the popover
gutternumber4The gap (in pixels) to apply between the popover and the target. Used by popper.js
usePortalbooleanfalseIf true the popover is displayed with a Portal. Rendering content inside a Portal allows the popover content to escape the physical bounds of its parent while still being positioned correctly relative to its target
onOpen() => voidCallback fired when the popover opens
onClose() => voidCallback fired when the popover closes

Other Props#

  • PopoverContent composes Box and has the ability to smartly position itself. Thanks to popper.js.
  • PopoverArrow, PopoverHeader, PopoverFooter and PopoverBody composes Box.
  • PopoverCloseButton composes CloseButton component.