Popover is a floating view that contains a task related to the content on screen. It can be triggered when the user clicks or focuses on an element, typically Button or IconButton. It can also be triggered automatically, as in the case of user education. Popover is non-modal and can be dismissed by interacting with another part of the screen or an item within Popover.
Popover is most appropriate for desktop screens and can contain a variety of elements, such as Button and Images. Popover is also the container used to construct more complex elements like Dropdown and the board picker, pictured below, which allow people to choose the board to save a Pin to.
also known as Flyout
Props
Usage guidelines
- Providing additional information for related context without cluttering the surface of a workflow.
- Accommodating a variety of features, such as Buttons, Images or SearchFields, that are not available in Dropdown.
Best practices
Use Popover to display a lightweight task related to the content on screen.
Use Popover to communicate critical information, such as an error or interaction feedback. Instead, use the error supplied directly to the form element. See related to learn more.
Accessibility
Keyboard interaction
- When Popover opens, focus moves to the first focusable element in the Popover container.
- Popovers are also a focus trap, so users should only be able to interact with the content inside the Popover container.
- Popover should always be dismissible using the ESC key. It could also be dismissed by interacting with another part of the screen, or by interacting with an element inside Popover.
- When Popover is closed, focus returns to the anchor element that triggered Popover.
ARIA attributes
We recommend passing the following ARIA attribute to Popover for a better screen reader experience:
accessibilityLabel
: describes the main purpose of a Popover for the screen reader. Should be unique and concise. For example, "Save to board" instead of "Popover". It populates aria-label.accessibilityDismissButtonLabel
: describes the purpose of the dismiss button on Popover for the screen reader. Should be clear and concise. For example, "Close board selection popover" instead of "Close".
To further assist screen readers, we recommend passing the following ARIA attributes to the anchor element:
accessibilityHaspopup
: informs the screen reader that there’s a Popover attached to the anchor element. It populates aria-haspopup.accessibilityExpanded
: informs the screen reader whether Popover is currently open or closed. It populates aria-expanded.accessibilityControls
: match with theid
of the associated Popover whose contents or visibility are controlled by the interactive component so that screen reader users can identify the relationship between elements. It populates aria-controls.
For the role
prop, use:
- 'dialog' if the Popover is a dialog that prompts the user to enter information or requires a response.
- 'menu' if the Popover presents a list of choices to the user.
- 'listbox' if the Popover is a widget that allows the user to select one or more items (whose role is option) from a list. May also include a search option.
- 'tooltip' if the Popover is a simple contextual text bubble that displays a description on a feature.
import { useRef, useState } from 'react'; import { Box, Button, Flex, Image, Mask, Popover, SearchField, TapArea, Text, } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const [selectedBoard, setSelectedBoard] = useState('Fashion'); const anchorRef = useRef(null); return ( <Flex alignItems="start" height="100%" justifyContent="center" width="100%"> <Box padding={3}> <Flex alignItems="center" gap={{ row: 2, column: 0 }}> <Button ref={anchorRef} accessibilityControls="a11l-example" accessibilityExpanded={open} accessibilityHaspopup color="white" iconEnd="arrow-down" onClick={() => { setOpen((o) => !o); }} selected={open} size="lg" text={selectedBoard} /> <Button color="red" onClick={() => {}} size="lg" text="Save" /> </Flex> </Box> {open && ( <Popover accessibilityLabel="Save to board" anchor={anchorRef.current} forceDirection id="a11l-example" idealDirection="down" onDismiss={() => setOpen(false)} shouldFocus={false} // positionRelativeToAnchor={false} showDismissButton size="xl" > <Box width={300}> <Box flex="grow" marginBottom={8} marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 6, row: 0 }}> <Text align="center" color="default" weight="bold"> Save to board </Text> <SearchField accessibilityLabel="Search boards field" id="searchField" onChange={() => {}} placeholder="Search boards" size="lg" /> </Flex> </Box> <Box height={300} overflow="scrollY"> <Box marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 8, row: 0 }}> <Flex direction="column" gap={{ column: 4, row: 0 }}> <Text color="default" size="100"> Top choices </Text> {[ { url: 'https://i.ibb.co/s3PRJ8v/photo-1496747611176-843222e1e57c.webp', title: 'Fashion', alt: 'Thumbnail image: a white dress with red flowers', }, { url: 'https://i.ibb.co/swC1qpp/IMG-0494.jpg', title: 'Food', alt: 'Thumbnail image: a paella with shrimp, green peas, red peppers and yellow rice', }, { url: 'https://i.ibb.co/PFVF3JH/photo-1583847268964-b28dc8f51f92.webp', title: 'Home', alt: 'Thumbnail image: a living room with a white couch, two paints in the wall and wooden furniture', }, ].map(({ alt, title: imageTitle, url }) => ( <TapArea key={imageTitle} onTap={() => { setSelectedBoard(imageTitle); setOpen(false); }} > <Flex alignItems="center" gap={{ row: 2, column: 0 }}> <Box height={50} overflow="hidden" rounding={2} width={50} > <Mask rounding={2}> <Image alt={alt} color="rgb(231, 186, 176)" naturalHeight={50} naturalWidth={50} src={url} /> </Mask> </Box> <Text align="center" color="default" weight="bold"> {imageTitle} </Text> </Flex> </TapArea> ))} </Flex> </Flex> </Box> </Box> </Box> </Popover> )} </Flex> ); }
Localization
Be sure to localize all text strings. Note that localization can lengthen text by 20 to 30 percent.
Note that accessibilityDismissButtonLabel
is optional as DefaultLabelProvider provides default strings. Use custom labels if they need to be more specific.
import { useEffect, useRef, useState } from 'react'; import { Box, Button, DefaultLabelProvider, Flex, Layer, Popover, Text, } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const anchorRef = useRef(null); useEffect(() => { setOpen(true); }, []); return ( <DefaultLabelProvider labels={{ Popover: { accessibilityDismissButtonLabel: 'Popover verwerfen.', }, }} > <Flex height="100%" width="100%"> <Box alignItems="start" display="flex" justifyContent="center" padding={2} width="100%" > <Button ref={anchorRef} color="red" onClick={() => setOpen((value) => !value)} size="lg" text="Sparen" /> </Box> {open && ( <Layer> <Popover accessibilityLabel="An Bord speichern." anchor={anchorRef.current} idealDirection="down" onDismiss={() => setOpen(false)} positionRelativeToAnchor showDismissButton size={240} > <Box alignItems="center" display="flex" height={200} justifyContent="center" width={240} > <Text align="center">Inhalt</Text> </Box> </Popover> </Layer> )} </Flex> </DefaultLabelProvider> ); }
Variants
Size
The maximum width of Popover. Popover has different size configurations:
"xs"
: 180px"sm"
: 230px"md"
: 284px"lg"
: 320px"xl"
: 360pxnumber
: Use this prop to create custom-size Popovers in pixels.flexible
: Use this configuration for larger Popovers. Without a defined maximum width, Popover grows to fit the content inchildren
.
We recommend using "xs
" for education Popovers and "xl
" for more complex Popovers. Avoid using other configurations whenever possible as they are legacy sizes.
Anchor
Popover requires a reference element, typically
Button or
IconButton, to set its position. The anchor
ref can be directly set on the reference component itself. If the components don’t support ref
, the anchor ref can be set to a parent
Box.
Popover calculates its position based on the bounding box of the anchor
. Therefore, the anchor
ref should only include the trigger element itself, usually
Button or
IconButton, or the specific feature component that requires an educational Popover.
import { useEffect, useRef, useState } from 'react'; import { Box, Button, Flex, Layer, Popover, Text } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const anchorRef = useRef(null); useEffect(() => { setOpen(true); }, []); return ( <Flex height="100%" width="100%"> <Box alignItems="start" display="flex" justifyContent="center" padding={2} width="100%" > <Button ref={anchorRef} color="red" onClick={() => setOpen((value) => !value)} size="lg" text="Save" /> </Box> {open && ( <Layer> <Popover anchor={anchorRef.current} idealDirection="down" onDismiss={() => {}} positionRelativeToAnchor size={400} > <Box alignItems="center" display="flex" height={200} justifyContent="center" width={300} > <Text align="center">Content</Text> </Box> </Popover> </Layer> )} </Flex> ); }
We highly recommend including a dismiss button on all Popovers with showDismissButton
. This improves accessibility and gives users an immediate action for closing Popover. A label for the button can be provided with the accessibilityDismissButtonLabel
prop. Don't forget to localize this label as well.
import { useRef, useState } from 'react'; import { Box, Button, Flex, Image, Mask, Popover, SearchField, TapArea, Text, } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const [selectedBoard, setSelectedBoard] = useState('Fashion'); const anchorRef = useRef(null); return ( <Flex alignItems="start" height="100%" justifyContent="center" width="100%"> <Box padding={3}> <Flex alignItems="center" gap={{ row: 2, column: 0 }}> <Button ref={anchorRef} accessibilityControls="a11l-example" accessibilityExpanded={open} accessibilityHaspopup color="white" iconEnd="arrow-down" onClick={() => { setOpen((o) => !o); }} selected={open} size="lg" text={selectedBoard} /> <Button color="red" onClick={() => {}} size="lg" text="Save" /> </Flex> </Box> {open && ( <Popover accessibilityLabel="Save to board" anchor={anchorRef.current} forceDirection id="a11l-example" idealDirection="down" onDismiss={() => setOpen(false)} shouldFocus={false} // positionRelativeToAnchor={false} showDismissButton size="xl" > <Box width={300}> <Box flex="grow" marginBottom={8} marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 6, row: 0 }}> <Text align="center" color="default" weight="bold"> Save to board </Text> <SearchField accessibilityLabel="Search boards field" id="searchField" onChange={() => {}} placeholder="Search boards" size="lg" /> </Flex> </Box> <Box height={300} overflow="scrollY"> <Box marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 8, row: 0 }}> <Flex direction="column" gap={{ column: 4, row: 0 }}> <Text color="default" size="100"> Top choices </Text> {[ { url: 'https://i.ibb.co/s3PRJ8v/photo-1496747611176-843222e1e57c.webp', title: 'Fashion', alt: 'Thumbnail image: a white dress with red flowers', }, { url: 'https://i.ibb.co/swC1qpp/IMG-0494.jpg', title: 'Food', alt: 'Thumbnail image: a paella with shrimp, green peas, red peppers and yellow rice', }, { url: 'https://i.ibb.co/PFVF3JH/photo-1583847268964-b28dc8f51f92.webp', title: 'Home', alt: 'Thumbnail image: a living room with a white couch, two paints in the wall and wooden furniture', }, ].map(({ alt, title: imageTitle, url }) => ( <TapArea key={imageTitle} onTap={() => { setSelectedBoard(imageTitle); setOpen(false); }} > <Flex alignItems="center" gap={{ row: 2, column: 0 }}> <Box height={50} overflow="hidden" rounding={2} width={50} > <Mask rounding={2}> <Image alt={alt} color="rgb(231, 186, 176)" naturalHeight={50} naturalWidth={50} src={url} /> </Mask> </Box> <Text align="center" color="default" weight="bold"> {imageTitle} </Text> </Flex> </TapArea> ))} </Flex> </Flex> </Box> </Box> </Box> </Popover> )} </Flex> ); }
With Layer
Popover is typically used within
Layer. Layer renders Popover outside the DOM hierarchy of the parent allowing it to overlay surrounding content. Popover calculates its position based on the bounding box of the anchor
. Within Layer, Popover no longer shares a relative root with the anchor
and requires disablePortal=true
to disable its internal Layer.
Using Layer
with Popover eliminates the need to use z-index
to solve stacking context conflicts. Popovers within Modals and OverlayPanels with z-indexes don't require zIndex
in Layer
.
import { Fragment, useEffect, useRef, useState } from 'react'; import { Box, Button, FixedZIndex, Flex, Image, Layer, Mask, OverlayPanel, Popover, SearchField, TapArea, Text, TextArea, } from 'gestalt'; const images = [ { url: 'https://i.ibb.co/s3PRJ8v/photo-1496747611176-843222e1e57c.webp', title: 'Fashion', alt: 'Thumbnail image: a white dress with red flowers', }, { url: 'https://i.ibb.co/swC1qpp/IMG-0494.jpg', title: 'Food', alt: 'Thumbnail image: a paella with shrimp, green peas, red peppers and yellow rice', }, { url: 'https://i.ibb.co/PFVF3JH/photo-1583847268964-b28dc8f51f92.webp', title: 'Home', alt: 'Thumbnail image: a living room with a white couch, two paints in the wall and wooden furniture', }, ]; function SearchBoardField() { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.focus(); }, []); return ( <SearchField ref={ref} accessibilityLabel="Search boards field" id="searchField" onChange={() => {}} placeholder="Search boards" size="lg" /> ); } function List({ handleImageTap, title }) { return ( <Flex direction="column" gap={{ column: 4, row: 0 }}> <Text color="default" size="100"> {title} </Text> <Flex direction="column" gap={{ column: 4, row: 0 }}> {images.map(({ alt, title: imageTitle, url }) => ( <TapArea key={imageTitle} onTap={() => { handleImageTap(imageTitle); }} rounding={2} > <Flex alignItems="center" gap={{ row: 2, column: 0 }}> <Box height={50} overflow="hidden" rounding={2} width={50}> <Mask rounding={2}> <Image alt={alt} color="rgb(231, 186, 176)" naturalHeight={50} naturalWidth={50} src={url} /> </Mask> </Box> <Text align="center" color="default" weight="bold"> {imageTitle} </Text> </Flex> </TapArea> ))} </Flex> </Flex> ); } function SelectBoard() { const [openPopover, setOpenPopover] = useState(false); const [selectedBoard, setSelectedBoard] = useState('Fashion'); const anchorRef = useRef(null); const handleImageTap = (imageTitle) => { setSelectedBoard(imageTitle); setOpenPopover(false); }; return ( <Fragment> <Flex direction="column" gap={{ column: 2, row: 0 }}> <Text size="100">Board</Text> <Button ref={anchorRef} accessibilityControls="popover-search-board-3" accessibilityExpanded={openPopover} accessibilityHaspopup iconEnd="arrow-down" onClick={() => setOpenPopover(!openPopover)} text={selectedBoard} /> </Flex> {openPopover && ( <Layer> <Popover anchor={anchorRef.current} id="popover-search-board-3" idealDirection="down" onDismiss={() => setOpenPopover(false)} positionRelativeToAnchor={false} showDismissButton size="xl" > <Box width={360}> <Box flex="grow" marginBottom={8} marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 6, row: 0 }}> <Text align="center" color="default" weight="bold"> Save to board </Text> <SearchBoardField /> </Flex> </Box> <Box height={300} overflow="scrollY"> <Box marginEnd={4} marginStart={4}> <Flex direction="column" gap={{ column: 8, row: 0 }}> <List handleImageTap={handleImageTap} title="Top choices" /> <List handleImageTap={handleImageTap} title="All boards" /> </Flex> </Box> </Box> </Box> </Popover> </Layer> )} </Fragment> ); } export default function Example() { const [showSheet, setShowSheet] = useState(false); return ( <Box padding={6}> <Button accessibilityControls="popover-overlaypanel" accessibilityExpanded={showSheet} accessibilityHaspopup onClick={() => setShowSheet(true)} size="lg" text="Edit Pin" /> {showSheet && ( <Layer zIndex={new FixedZIndex(11)}> <OverlayPanel accessibilityDismissButtonLabel="Close edit Pin overlay panel" accessibilityLabel="Edit your Pin details" footer={ <Flex> <Flex.Item flex="grow"> <Button color="white" onClick={() => setShowSheet(false)} size="lg" text="Delete" /> </Flex.Item> <Flex gap={{ column: 0, row: 2 }}> <Button onClick={() => setShowSheet(false)} size="lg" text="Cancel" /> <Button color="red" onClick={() => setShowSheet(false)} size="lg" text="Done" type="submit" /> </Flex> </Flex> } heading="Edit Pin" onDismiss={() => setShowSheet(false)} size="lg" > <Box display="flex" height={400} id="popover-overlaypanel" paddingX={8} > <Flex gap={{ row: 8, column: 0 }} width="100%"> <Box paddingX={2} rounding={4} width={200}> <Mask rounding={4}> <Image alt="Tropic greens: The taste of Petrol and Porcelain | Interior design, Vintage Sets and Unique Pieces agave" color="rgb(231, 186, 176)" naturalHeight={751} naturalWidth={564} src="https://i.ibb.co/7bQQYkX/stock2.jpg" /> </Mask> </Box> <Flex.Item flex="grow"> <Flex direction="column" gap={{ column: 8, row: 0 }}> <SelectBoard /> <TextArea id="note" label="Note" onChange={() => {}} placeholder="Add note" value="" /> </Flex> </Flex.Item> </Flex> </Box> </OverlayPanel> </Layer> )} </Box> ); }
Ideal direction
Pass in idealDirection
to specify the preferred position of Popover relative to the anchor, such as
Button or
IconButton, that triggered it.
Adjust the idealDirection
as necessary to ensure the visibility of Popover and its contextual information. The default direction is "up", although Popover should be center-aligned directly below the element in most cases. The actual position may change given the available space around the anchor element.
import { useRef, useState } from 'react'; import { Box, ButtonToggle, Flex, Label, Layer, Popover, SelectList, Switch, Text, } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const [direction, setDirection] = useState('down'); const [forceDirection, setForceDirection] = useState(false); const anchorRef = useRef(null); return ( <Flex alignItems="center" direction="column" gap={6} height="100%" justifyContent="center" width="100%" > <Flex gap={6}> <SelectList id="idealDir" label="idealDirection" onChange={({ value }) => { const dirValue = value; setDirection(dirValue); }} > {[ { label: 'Up', value: 'up' }, { label: 'Down', value: 'down' }, { label: 'Right', value: 'right' }, { label: 'Left', value: 'left' }, ].map(({ label, value }) => ( <SelectList.Option key={label} label={label} value={value} /> ))} </SelectList> <Flex alignItems="center" direction="column" gap={2}> <Label htmlFor="idealDirLabel"> <Text size="100"> {forceDirection ? 'forceDirection: true' : 'forceDirection: false'} </Text> </Label> <Switch id="idealDirSwitch" onChange={() => setForceDirection((currVal) => !currVal)} switched={forceDirection} /> </Flex> </Flex> <ButtonToggle ref={anchorRef} onClick={() => setOpen((value) => !value)} selected={open} size="lg" text={open ? 'Showing Popover' : 'Open Popover'} /> {open && ( <Layer> <Popover anchor={anchorRef.current} forceDirection={forceDirection} idealDirection={direction} onDismiss={() => {}} positionRelativeToAnchor={false} > <Box alignItems="center" display="flex" height={100} justifyContent="center" > <Text align="center">Content</Text> </Box> </Popover> </Layer> )} </Flex> ); }
Within scrolling containers
Popover always remains attached to its anchor when scrolling. However Popover may float outside of the scrolling container without adjusting itself (shift / flip) when reached to the edges if:
- Popover is rendered through portal using
disablePortal={true}
prop- Layer component
- the scrolling container is not positioned relatively (
position: relative
)
For some use cases that can be the indeneded behavior.
If Popover has to respect the edges of its scrolling container, the scrolling has to have position CSS rule set to relative
. Popover's disablePortal
prop is by default true
. That's why in this case disablePortal
should not be set to false
.
import { useEffect, useRef, useState } from 'react'; import { Box, ButtonLink, Flex, Popover, Text } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const anchorRef = useRef(null); const viewRef = useRef(null); useEffect(() => { setOpen(true); }, []); return ( <Box alignItems="center" color="lightWash" display="flex" height="100%" justifyContent="center" padding={6} width="100%" > <Box height={200} overflow="auto" position="relative" // this prevents Popover from overflowing the container > <Box ref={viewRef} color="default" padding={4} position="relative" width={600} > <Flex alignItems="center" gap={{ column: 0, row: 4 }}> <Box width={220}> <Text> You need to add your data source URL to Pinterest so we can access your data source file and create Pins for your products. Before you do this, make sure you have prepared your data source and that you have claimed your website. If there are any errors with your data source file, you can learn how to troubleshoot them below. After you click Create Pins, you'll land back at the main data source page while your feed is being processed. Wait for a confirmation email from Pinterest about the status of your data source submission. </Text> </Box> <ButtonLink ref={anchorRef} href="https://help.pinterest.com/en/business/article/data-source-ingestion" iconEnd="visit" onClick={() => setOpen(false)} size="lg" target="blank" text="Help" /> </Flex> {open && ( <Popover anchor={anchorRef.current} disablePortal idealDirection="right" onDismiss={() => {}} size="xs" > <Box alignItems="center" display="flex" height={100} justifyContent="center" > <Text align="center">Content</Text> </Box> </Popover> )} </Box> </Box> </Box> ); }
Visibility on page load
Popover's positioning algorithm requires that the anchor element renders before Popover is rendered. If Popover should be visible on page load, use useEffect
to toggle the visibility after the first render.
import { useEffect, useRef, useState } from 'react'; import { Box, Flex, IconButton, Layer, Popover, Text } from 'gestalt'; export default function Example() { const [open, setOpen] = useState(false); const anchorRef = useRef(null); useEffect(() => { setOpen(true); }, []); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <IconButton ref={anchorRef} accessibilityLabel="Default IconButton" icon="pin" iconColor="darkGray" onClick={() => {}} size="lg" /> {open && ( <Layer> <Popover anchor={anchorRef.current} idealDirection="down" onDismiss={() => {}} positionRelativeToAnchor={false} size="xs" > <Box alignItems="center" display="flex" height={100} justifyContent="center" > <Text align="center">Content</Text> </Box> </Popover> </Layer> )} </Flex> ); }
Writing
- Be clear and predictable so that people anticipate what will happen when they interact with an item.
- Focus on the action by beginning with a verb.
- Use succinct and scannable language.
- Use sentence case while always capitalizing the word “Pin.”
- Describe the interface element, like “button,” “icon” or “menu” in education messaging, unless it’s absolutely necessary for clarity.
- Use words like “click” or “tap” in education messaging, if possible, or assume universal accessibility.
- Use Popover to communicate critical information, such as an error or interaction feedback.
Component quality checklist
Quality item | Status | Status description | |
---|---|---|---|
Figma Library | Ready | Component is available in Figma for web and mobile web. | |
Responsive Web | Ready | Component responds to changing viewport sizes in web and mobile web. | |
Issues | This component has known issues. |
Related
PopoverEducational
Popover used for education or onboarding experiences.
Dropdown
Dropdown is an element constructed using Popover as its container. Use Dropdown to display a list of actions or options in a Popover.
Toast
Toast provides feedback on an interaction. One example of Toast is the confirmation that appears when a Pin has been saved. Toasts appear at the bottom of a desktop screen or top of a mobile screen instead of being attached to any particular element on the interface.
Tooltip
Tooltip describes the function of an interactive element, typically
IconButton, on hover. While Popovers offer broader content options, such as
Button and
Images, Tooltips are purely text-based.
Layer
Layer renders Popover outside the DOM hierarchy of the parent and prevents surrounding components overlaying Popover. See the
with Layer variant to learn more.
ScrollBoundaryContainer
ScrollBoundaryContainer is needed for proper positioning when Popover is anchored to an element that is located within a scrolling container. The use of ScrollBoundaryContainer ensures that Popover remains attached to its anchor when scrolling. See the
within scrolling containers variant to learn more.