Skip to content

Popover

Popover displays additional information without interrupting user flow.

Basic Usage

To implement the Popover component, you need to import it first:

import { Popover, PopoverWrapper } from '@react-ui-org/react-ui';

And use it:

React.createElement(() => {
  const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
  // All inline styles in this example are for demonstration purposes only.
  return (
    <div
      style={{
        display: 'grid',
        placeContent: 'center',
        minWidth: '20rem',
        minHeight: '10rem',
      }}
    >
      <PopoverWrapper>
        <Button
          aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
          label="Want to see a popover? Click me!"
          onClick={() => setIsPopoverOpen(!isPopoverOpen)}
        />
        {isPopoverOpen && (
          <Popover id="my-popover">
            Hello there!
          </Popover>
        )}
      </PopoverWrapper>
    </div>
  );
});

See API for all available options.

Placement

Available placements are: top, right, bottom, and left. Additionally, all basic placements can be aligned to the center (default, no suffix), start (e.g. top-start), or end (e.g. bottom-end). Check Popover API for the complete list of accepted values.

React.createElement(() => {
  const [align, setAlign] = React.useState('');
  // All inline styles in this example are for demonstration purposes only.
  return (
    <>
      <Toolbar align="baseline">
        <ToolbarItem>
          <span id="alignment-options-label">Alignment:</span>
        </ToolbarItem>
        <ToolbarItem>
          <ButtonGroup aria-labelledby="alignment-options-label">
            <Button
              aria-pressed={align === '-start'}
              color={align === '-start' ? 'selected' : 'secondary'}
              label="start"
              onClick={() => setAlign('-start')}
            />
            <Button
              aria-pressed={align === ''}
              color={align === '' ? 'selected' : 'secondary'}
              label="center"
              onClick={() => setAlign('')}
            />
            <Button
              aria-pressed={align === '-end'}
              color={align === '-end' ? 'selected' : 'secondary'}
              label="end"
              onClick={() => setAlign('-end')}
            />
          </ButtonGroup>
        </ToolbarItem>
      </Toolbar>
      <div
        style={{
          display: 'grid',
          placeContent: 'center',
          minWidth: '20rem',
          minHeight: '15rem',
        }}
      >
        <PopoverWrapper>
          <docoff-placeholder bordered aria-describedby="my-popover-top">
            Popovers
            <br />
            all day long…
          </docoff-placeholder>
          <Popover id="my-popover-top" placement={`top${align}`}>
            Top side
          </Popover>
          <Popover id="my-popover-right" placement={`right${align}`}>
            Right side
          </Popover>
          <Popover id="my-popover-bottom" placement={`bottom${align}`}>
            Bottom side
          </Popover>
          <Popover id="my-popover-left" placement={`left${align}`}>
            Left side
          </Popover>
        </PopoverWrapper>
      </div>
    </>
  );
});

Smart Positioning

Popover needs a positioning anchor β€” a parent element with position: relative or position: absolute. PopoverWrapper provides one, and it does something more: when your trigger and Popover are wrapped in PopoverWrapper, Floating UI runs automatically with flip() and shift() middleware and autoUpdate. No extra imports or setup are required.

<PopoverWrapper>
  <Button
    aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
    label="Want to see a popover? Click me!"
    onClick={() => setIsPopoverOpen(!isPopoverOpen)}
  />
  {isPopoverOpen && <Popover id="my-popover">Hello there!</Popover>}
</PopoverWrapper>

If your CSS already has a positioned ancestor wrapping your trigger, PopoverWrapper is not strictly required β€” but you lose automatic flip and shift.

πŸ–± Try scrolling the example to see how Popover flips to stay in view. The suggested placement is what you set; Floating UI may use a different final placement after applying flip and shift.

React.createElement(() => {
  const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
  const [placement, setPlacement] = React.useState('top');
  const placementOptions = [
    'top',
    'top-start',
    'top-end',
    'right',
    'right-start',
    'right-end',
    'bottom',
    'bottom-start',
    'bottom-end',
    'left',
    'left-start',
    'left-end',
  ];
  // All inline styles in this example are for demonstration purposes only.
  return (
    <>
      <Toolbar>
        <ToolbarItem>
          <SelectField
            label="Suggested placement:"
            onChange={e => setPlacement(e.target.value)}
            options={placementOptions.map((el) => ({
              label: el,
              value: el,
            }))}
            value={placement}
          />
        </ToolbarItem>
      </Toolbar>
      <div
        style={{
          width: '40rem',
          maxWidth: '100%',
          height: '12rem',
          overflow: 'auto',
        }}
      >
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
            height: '20rem',
            justifyContent: 'center',
            width: '60rem',
          }}
        >
          <PopoverWrapper>
            <Button
              aria-describedby={isPopoverOpen ? 'my-smart-popover' : undefined}
              label="Trigger Popover"
              onClick={() => setIsPopoverOpen(!isPopoverOpen)}
            />
            {isPopoverOpen && (
              <Popover
                id="my-smart-popover"
                placement={placement}
              >
                Auto-repositioning Popover
              </Popover>
            )}
          </PopoverWrapper>
        </div>
      </div>
    </>
  );
});

When Popover is rendered outside a PopoverWrapper β€” for example inside a portal or a custom layout β€” pass the anchor DOM element directly via the referenceElement prop.

Head to PopoverWrapper API for all available options.

Rendering in a React Portal

When a Popover is placed inside a CSS stacking context β€” for example, inside a layout element that uses transform, opacity, or an explicit z-index β€” it may be covered by other page elements regardless of its own z-index. This is a fundamental CSS limitation: z-index only works within the same stacking context.

The solution is to render the Popover in a React portal attached to a container at the root of the document, where it is free of any stacking context constraints. Use the portalId prop to specify the ID of the portal container.

πŸ‘‰ PopoverWrapper is still needed when using a portal β€” it provides the reference element for Floating UI. The portal only determines where the Popover is rendered in the DOM, not how it is positioned.

In the example below, the trigger is inside a container with transform (a stacking context). The sibling element below it has z-index: 2 and covers the Popover without a portal.

πŸ–± Enable Use portal to see the Popover break free of the stacking context. Then try scrolling the whole page to see how Popover flips to stay in view.

React.createElement(() => {
  const PORTAL_ID = 'my-popover-portal';
  const [isOpen, setIsOpen] = React.useState(false);
  const [usePortal, setUsePortal] = React.useState(false);

  // All inline styles in this example are for demonstration purposes only.
  return (
    <>
      <Toolbar>
        <ToolbarItem>
          <Toggle
            checked={usePortal}
            label="Use portal"
            onChange={() => { setIsOpen(false); setUsePortal(v => !v); }}
          />
        </ToolbarItem>
      </Toolbar>
      <div
        style={{
          display: 'flex',
          justifyContent: 'center',
          paddingTop: '1.5rem',
          transform: 'translateZ(0)',
        }}
      >
        <PopoverWrapper>
          <Button
            aria-describedby={isOpen ? 'my-portal-popover' : undefined}
            label="Open Popover"
            onClick={() => setIsOpen(v => !v)}
          />
          {isOpen && (
            <Popover
              id="my-portal-popover"
              placement="bottom"
              portalId={usePortal ? PORTAL_ID : undefined}
            >
              Hello there!
            </Popover>
          )}
        </PopoverWrapper>
      </div>
      <div
        style={{
          position: 'relative',
          display: 'grid',
          placeContent: 'center',
          minHeight: '5rem',
          marginTop: '2.25rem',
          borderRadius: 'var(--rui-dimension-radius-2, 0.2rem)',
          background: 'var(--rui-color-background-danger, #ffe3e3)',
        }}
      >
        Sibling element (z-index: 2)
      </div>
      <div id={PORTAL_ID} />
    </>
  );
});

For apps that use many Popovers, configure this globally via GlobalPropsProvider so every Popover uses the portal automatically:

{/* Once in the app root, outside any stacking contexts: */}
<div id="popover-portal" />

<GlobalPropsProvider globalProps={{ Popover: { portalId: 'popover-portal' } }}>
  {/* All Popovers in the subtree use the portal */}
</GlobalPropsProvider>

Forwarding HTML Attributes

In addition to the options below in the component's API section, you can specify React synthetic events or any HTML attribute you like. All attributes that don't interfere with the API are forwarded to the root <div> HTML element. This enables making the component interactive and helps to improve its accessibility.

πŸ‘‰ Refer to the MDN reference for the full list of supported attributes of the div element.

Forwarding ref

If you provide ref, it is forwarded to the root native HTML <div> element.

API

PopoverWrapper API

Theming

Custom Property Description
--rui-Popover__width Popover width
--rui-Popover__padding Popover padding
--rui-Popover__border-width Border width
--rui-Popover__border-color Border color
--rui-Popover__border-radius Corner radius
--rui-Popover__color Text color
--rui-Popover__background-color Background color
--rui-Popover__box-shadow Popover box shadow
--rui-Popover__z-index Popover z-index (RADES default: 150)