Skip to main content
import { Popover, Button } from 'reshaped';

function Example() {
  return (
    <Popover position="bottom">
      <Popover.Trigger>
        {(attributes) => <Button attributes={attributes}>Open Popover</Button>}
      </Popover.Trigger>
      
      <Popover.Content>
        <View gap={2} padding={4}>
          <Text variant="body-2" weight="bold">Popover Title</Text>
          <Text>Additional information in the popover</Text>
          <Button size="small">Action</Button>
        </View>
      </Popover.Content>
    </Popover>
  );
}

Usage

Popovers display rich content in a floating container attached to a trigger element. They’re ideal for contextual information, actions, or custom interactive content.

Props

position
string
Position relative to the trigger element.Options: "bottom", "bottom-start", "bottom-end", "top", "top-start", "top-end", "start", "start-top", "start-bottom", "end", "end-top", "end-bottom"Default: "bottom"
active
boolean
Controlled state for the popover visibility.
<Popover active={isOpen} onClose={() => setIsOpen(false)}>
defaultActive
boolean
Initial active state for uncontrolled mode.
<Popover defaultActive>
triggerType
string
Event that triggers the popover.Options: "click", "hover", "focus"Default: "click"
width
string
Width of the popover content. Accepts CSS values or "trigger" to match trigger width.
<Popover width="400px" />
<Popover width="trigger" />
padding
number
Padding inside the popover content. Value is a unit token multiplier.
<Popover padding={4}>
elevation
string
Shadow elevation level for the popover.Options: "raised", "overlay"Default: "overlay"
borderRadius
string
Border radius for the popover.Options: "small", "medium"
contentGap
number
Gap between the trigger and content in pixels.
contentShift
number
Shift the content on the secondary axis.
contentMaxHeight
string
Maximum height for scrollable content.
<Popover contentMaxHeight="300px">
contentZIndex
number
Custom z-index for the popover content.
forcePosition
boolean
Force the specified position without fallbacks.
fallbackPositions
Position[] | false
Fallback positions when content doesn’t fit. Set to false to disable.
<Popover fallbackPositions={['top', 'bottom-start']} />
<Popover fallbackPositions={false} />
fallbackAdjustLayout
boolean
Adjust size and position when fallbacks don’t work.
disableHideAnimation
boolean
Disable the hide animation.
disableCloseOnOutsideClick
boolean
Prevent closing when clicking outside.
autoFocus
boolean
default:"true"
Focus first focusable element when opened.
onOpen
() => void
Callback when the popover opens.
onClose
(args: { reason?: string }) => void
Callback when the popover closes. Reason can be "outside-click", "escape-key", etc.
containerRef
React.RefObject<HTMLElement>
Container element for positioning.
positionRef
React.RefObject<HTMLElement>
Element to calculate position against.
initialFocusRef
React.RefObject<HTMLElement>
Element to focus when opened.
instanceRef
React.Ref<PopoverInstance>
Ref to access popover methods.
const popoverRef = React.useRef();

<Popover instanceRef={popoverRef}>

// Later:
popoverRef.current.open();
popoverRef.current.close();
popoverRef.current.updatePosition();

Subcomponents

Popover.Trigger

Render function that provides attributes for the trigger element.
<Popover.Trigger>
  {(attributes) => <Button attributes={attributes}>Open</Button>}
</Popover.Trigger>

Popover.Content

Container for the popover content.
<Popover.Content>
  {/* Your content */}
</Popover.Content>
className
string
Additional CSS class for the content element.
attributes
Attributes<'div'>
Additional HTML attributes for the content element.

Popover.Dismissible

Pre-styled header with close button.
<Popover.Dismissible closeAriaLabel="Close" onClose={handleClose}>
  <Text variant="body-2" weight="bold">Popover Title</Text>
</Popover.Dismissible>

Examples

Basic Popover

<Popover>
  <Popover.Trigger>
    {(attributes) => <Button attributes={attributes}>Info</Button>}
  </Popover.Trigger>
  
  <Popover.Content>
    <View gap={2} padding={3}>
      <Text>Additional information here</Text>
    </View>
  </Popover.Content>
</Popover>

With Dismissible Header

function InfoPopover() {
  const [active, setActive] = React.useState(false);

  return (
    <Popover active={active} onClose={() => setActive(false)}>
      <Popover.Trigger>
        {(attributes) => (
          <Button attributes={attributes} onClick={() => setActive(true)}>
            Details
          </Button>
        )}
      </Popover.Trigger>
      
      <Popover.Content>
        <View gap={3}>
          <Popover.Dismissible
            closeAriaLabel="Close"
            onClose={() => setActive(false)}
          >
            <Text variant="title-6">More Information</Text>
          </Popover.Dismissible>
          
          <Text>Detailed content goes here</Text>
        </View>
      </Popover.Content>
    </Popover>
  );
}

Positions

// Bottom positions
<Popover position="bottom-start" />
<Popover position="bottom" />
<Popover position="bottom-end" />

// Top positions
<Popover position="top-start" />
<Popover position="top" />
<Popover position="top-end" />

// Side positions
<Popover position="start" />
<Popover position="start-top" />
<Popover position="start-bottom" />
<Popover position="end" />
<Popover position="end-top" />
<Popover position="end-bottom" />

Trigger Types

// Click (default)
<Popover triggerType="click">

// Hover
<Popover triggerType="hover">

// Focus
<Popover triggerType="focus">

Custom Width

// Fixed width
<Popover width="400px">

// Match trigger width
<Popover width="trigger">

// Full width
<Popover width="100%">

User Profile Card

function UserProfilePopover({ user }) {
  return (
    <Popover triggerType="hover" width="300px">
      <Popover.Trigger>
        {(attributes) => (
          <Button variant="ghost" attributes={attributes}>
            <View direction="row" gap={2} align="center">
              <Image
                src={user.avatar}
                width="24px"
                height="24px"
                borderRadius="full"
              />
              <Text>{user.name}</Text>
            </View>
          </Button>
        )}
      </Popover.Trigger>
      
      <Popover.Content>
        <View gap={3} padding={4}>
          <View direction="row" gap={3} align="center">
            <Image
              src={user.avatar}
              width="60px"
              height="60px"
              borderRadius="full"
            />
            <View.Item grow>
              <Text variant="body-2" weight="bold">{user.name}</Text>
              <Text variant="body-3" color="neutral-faded">{user.role}</Text>
            </View.Item>
          </View>
          
          <Text variant="body-3">{user.bio}</Text>
          
          <View direction="row" gap={2}>
            <Button size="small">View Profile</Button>
            <Button size="small" variant="outline">Message</Button>
          </View>
        </View>
      </Popover.Content>
    </Popover>
  );
}

Form Popover

function FilterPopover() {
  const [filters, setFilters] = React.useState({});

  return (
    <Popover width="350px">
      <Popover.Trigger>
        {(attributes) => (
          <Button attributes={attributes} icon={IconFilter}>
            Filters
          </Button>
        )}
      </Popover.Trigger>
      
      <Popover.Content>
        <View gap={3} padding={4}>
          <Text variant="body-2" weight="bold">Filter Results</Text>
          
          <TextField
            label="Search"
            value={filters.search}
            onChange={(e) => setFilters({ ...filters, search: e.target.value })}
          />
          
          <Select
            label="Category"
            value={filters.category}
            onChange={(e) => setFilters({ ...filters, category: e.target.value })}
          >
            <option value="all">All Categories</option>
            <option value="design">Design</option>
            <option value="development">Development</option>
          </Select>
          
          <View direction="row" gap={2} justify="end">
            <Button size="small" variant="ghost" onClick={handleReset}>
              Reset
            </Button>
            <Button size="small" onClick={handleApply}>
              Apply
            </Button>
          </View>
        </View>
      </Popover.Content>
    </Popover>
  );
}

Color Picker

function ColorPickerPopover({ value, onChange }) {
  const colors = [
    '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A',
    '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2',
  ];

  return (
    <Popover>
      <Popover.Trigger>
        {(attributes) => (
          <Button attributes={attributes}>
            <View
              width="24px"
              height="24px"
              borderRadius="small"
              attributes={{ style: { backgroundColor: value } }}
            />
          </Button>
        )}
      </Popover.Trigger>
      
      <Popover.Content>
        <View gap={2} padding={3}>
          <Text variant="body-3" weight="bold">Select Color</Text>
          <Grid columns="4" gap={2}>
            {colors.map((color) => (
              <Button
                key={color}
                variant="ghost"
                onClick={() => onChange(color)}
              >
                <View
                  width="32px"
                  height="32px"
                  borderRadius="small"
                  attributes={{ style: { backgroundColor: color } }}
                />
              </Button>
            ))}
          </Grid>
        </View>
      </Popover.Content>
    </Popover>
  );
}

Controlled Popover

function ControlledPopover() {
  const [active, setActive] = React.useState(false);

  return (
    <>
      <Popover
        active={active}
        onClose={() => setActive(false)}
      >
        <Popover.Trigger>
          {(attributes) => (
            <Button
              attributes={attributes}
              onClick={() => setActive(!active)}
            >
              Toggle
            </Button>
          )}
        </Popover.Trigger>
        
        <Popover.Content>
          <View gap={2} padding={3}>
            <Text>Popover content</Text>
            <Button size="small" onClick={() => setActive(false)}>
              Close
            </Button>
          </View>
        </Popover.Content>
      </Popover>
      
      <Button onClick={() => setActive(!active)}>
        External Toggle
      </Button>
    </>
  );
}

Accessibility

  • Uses role="dialog" for popover content
  • Traps focus within the popover when using triggerType="click"
  • Pressing Escape closes the popover
  • Trigger element has aria-expanded and aria-controls attributes
  • Automatically manages focus when opening and closing
  • Returns focus to trigger when closed
  • Ensure trigger has clear affordance (button styling)

Build docs developers (and LLMs) love