Popover
Popover displays rich content in a floating panel triggered by a click.
Open in Storybook
Import
import { Popover, PopoverHeader, PopoverBody } from "@tosui/react";
Basic usage
- Preview
- Code
<Popover
aria-label="Popover details"
content={
<PopoverBody>
This is the popover content.
</PopoverBody>
}
>
<Button>Click me</Button>
</Popover>
With header
- Preview
- Code
<Popover
aria-labelledby="popover-title"
content={
<>
<PopoverHeader>
<span id="popover-title">Popover Title</span>
</PopoverHeader>
<PopoverBody>
This popover has a header and body.
</PopoverBody>
</>
}
>
<Button>With Header</Button>
</Popover>
Accessible name
Give every popover an accessible name. Reference a visible heading with aria-labelledby, as shown above. If the panel has no visible heading, use aria-label.
The child must accept event and ARIA props so Popover can connect it to the panel. Use a Button, IconButton, or another focusable component that forwards those props.
Placement
- Preview
- Code
<HStack gap={4}>
<Popover
aria-label="Top popover"
placement="top"
content={<PopoverBody>Top</PopoverBody>}
>
<Button>Top</Button>
</Popover>
<Popover
aria-label="Bottom popover"
placement="bottom"
content={<PopoverBody>Bottom</PopoverBody>}
>
<Button>Bottom</Button>
</Popover>
<Popover
aria-label="Left popover"
placement="left"
content={<PopoverBody>Left</PopoverBody>}
>
<Button>Left</Button>
</Popover>
<Popover
aria-label="Right popover"
placement="right"
content={<PopoverBody>Right</PopoverBody>}
>
<Button>Right</Button>
</Popover>
</HStack>
Close on blur
By default, popover closes when clicking outside. Disable with closeOnBlur={false}.
<Popover
aria-label="Persistent popover"
closeOnBlur={false}
content={
<PopoverBody>
Click outside - I won't close!
</PopoverBody>
}
>
<Button>Stays Open</Button>
</Popover>
Controlled mode
Use isOpen, onOpen, and onClose for full control.
function ControlledPopover() {
const [isOpen, setIsOpen] = useState(false);
return (
<Popover
aria-label="Controlled popover"
isOpen={isOpen}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
content={
<PopoverBody>
<Button size="sm" onClick={() => setIsOpen(false)}>
Close
</Button>
</PopoverBody>
}
>
<Button>Controlled</Button>
</Popover>
);
}
Common patterns
Confirmation popover
<Popover
aria-label="Delete item"
content={
<>
<PopoverHeader>Delete item?</PopoverHeader>
<PopoverBody>
<Text mb={3}>This action cannot be undone.</Text>
<HStack gap={2} justify="end">
<Button size="sm" variant="ghost">Cancel</Button>
<Button size="sm" colorScheme="error">Delete</Button>
</HStack>
</PopoverBody>
</>
}
>
<Button variant="ghost" colorScheme="error">Delete</Button>
</Popover>
User profile Popover
<Popover
aria-label="John Doe profile"
content={
<PopoverBody>
<VStack gap={2} align="start">
<HStack gap={3}>
<Avatar name="John Doe" />
<Box>
<Text weight="medium">John Doe</Text>
<Text size="sm" color="foreground-muted">john@example.com</Text>
</Box>
</HStack>
<Divider />
<Button variant="ghost" size="sm" fullWidth>View Profile</Button>
<Button variant="ghost" size="sm" fullWidth>Settings</Button>
<Button variant="ghost" size="sm" fullWidth colorScheme="error">Sign Out</Button>
</VStack>
</PopoverBody>
}
>
<Button variant="ghost" aria-label="Open John Doe profile">
<Avatar name="John Doe" />
</Button>
</Popover>
Form popover
<Popover
aria-label="Quick add"
closeOnBlur={false}
content={
<>
<PopoverHeader>Quick Add</PopoverHeader>
<PopoverBody>
<VStack gap={3}>
<Input placeholder="Item name" />
<HStack gap={2} justify="end">
<Button size="sm" variant="ghost">Cancel</Button>
<Button size="sm">Add</Button>
</HStack>
</VStack>
</PopoverBody>
</>
}
>
<Button>Quick Add</Button>
</Popover>
Props reference
Popover
| Prop | Type | Default | Description |
|---|---|---|---|
| content | ReactNode | - | Popover content (required) |
| placement | "top" | "bottom" | "left" | "right" | "bottom" | Popover position |
| closeOnBlur | boolean | true | Close on outside click |
| isOpen | boolean | - | Controlled open state |
| onOpen | () => void | - | Callback when popover opens |
| onClose | () => void | - | Callback when popover closes |
| className | string | - | Additional CSS class on the dialog panel |
| aria-label | string | - | Accessible name for the dialog |
| aria-labelledby | string | - | ID of an element that labels the dialog |
| children | ReactNode | - | Focusable trigger element (required) |
PopoverHeader, PopoverBody
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Additional CSS class |
| children | ReactNode | - | Section content |
Accessibility
- The panel uses
role="dialog". - Give every popover an accessible name with
aria-labeloraria-labelledby. - The focusable trigger receives
aria-expanded,aria-haspopup="dialog", andaria-controls. Popover preserves other trigger attributes and adds the panel ID to an existingaria-controlsvalue. - Focus moves into the panel, stays inside it, and returns to the previously focused element when the panel closes.
- Escape closes the panel.
- The portal renders only in the browser and updates its position during scroll and resize events.
TypeScript
import { Popover, PopoverHeader, PopoverBody, type PopoverPlacement, type PopoverProps, type PopoverHeaderProps, type PopoverBodyProps } from "@tosui/react";