useOverlayTrigger

Handles the behavior and accessibility for an overlay trigger, e.g. a button that opens a popover, menu, or other overlay that is positioned relative to the trigger.

installyarn add @react-aria/overlays
version3.8.1
usageimport {useOverlayTrigger} from '@react-aria/overlays'

API#


useOverlayTrigger( props: OverlayTriggerProps, state: OverlayTriggerState, ref: RefObject<HTMLElement> ): OverlayTriggerAria

Features#


There is no built in way to create popovers or other types of overlays in HTML. useOverlayTrigger, combined with useOverlayPosition, helps achieve accessible overlays that can be styled as needed.

Note: useOverlayTrigger only handles the overlay itself. It should be combined with useDialog to create fully accessible popovers. For menus, see useMenuTrigger, which builds on useOverlayTrigger and provides additional functionality specific to menus.

  • Exposes overlay trigger and connects trigger to overlay with ARIA
  • Positions the overlay relative to the trigger when combined with useOverlayPosition
  • Hides content behind the overlay from screen readers when combined with useModal
  • Handles closing the overlay when interacting outside and pressing the Escape key, when combined with useOverlay

Anatomy#


An overlay trigger consists of a trigger element (e.g. button) and an overlay (e.g. popover, menu, listbox, etc.). useOverlayTrigger handles exposing the trigger and overlay to assistive technology with ARIA. It should be combined with useOverlay to handle closing the overlay, useModal to handle hiding content behind the overlay from screen readers, and optionally with useOverlayPosition to handle positioning the overlay relative to the trigger.

useOverlayTrigger returns props that you should spread onto the trigger element and the appropriate element:

NameTypeDescription
triggerPropsAriaButtonPropsProps for the trigger element.
overlayPropsHTMLAttributes<HTMLElement>Props for the overlay container element.

State is managed by the useOverlayTriggerState hook in @react-stately/overlays. The state object should be passed as an argument to useOverlayTrigger.

Example#


This example shows how to build a typical popover overlay that is positioned relative to a trigger button. The content of the popover is a dialog, built with useDialog.

The popover can be closed by clicking or interacting outside the popover, or by pressing the Escape key. This is handled by useOverlay. When the popover is closed, focus is restored back to its trigger button by a <FocusScope>.

Content outside the popover is hidden from screen readers by useModal. This improves the experience for screen reader users by ensuring that they don't navigate out of context. This is especially important when the popover is rendered into a portal at the end of the document, and the content just before it is unrelated to the original trigger.

To allow screen reader users to more easily dismiss the popover, a visually hidden <DismissButton> is added at the end of the dialog.

The application is contained in an OverlayProvider, which is used to hide the content from screen readers with aria-hidden while an overlay is open. In addition, each overlay must be contained in an OverlayContainer, which uses a React Portal to render the overlay at the end of the document body. If a nested overlay is opened, then the first overlay will also be set to aria-hidden, so that only the top-most overlay is accessible to screen readers.

Note: useModal only hides content within parent OverlayProvider components. However, if you have additional content in your application outside any OverlayProvider, then you should use the @react-aria/aria-modal-polyfill package to ensure that this content is hidden while modals are open as well. See the watchModals docs for more information.

import {useOverlayTriggerState} from '@react-stately/overlays';
import {
  DismissButton,
  OverlayContainer,
  OverlayProvider,
  useModal,
  useOverlay,
  useOverlayPosition,
  useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';

const Popover = React.forwardRef(({
  title,
  children,
  isOpen,
  onClose,
  style,
  ...otherProps
}, ref) => {
  // Handle interacting outside the dialog and pressing
  // the Escape key to close the modal.
  let { overlayProps } = useOverlay({
    onClose,
    isOpen,
    isDismissable: true
  }, ref);

  // Hide content outside the modal from screen readers.
  let { modalProps } = useModal();

  // Get props for the dialog and its title
  let { dialogProps, titleProps } = useDialog({}, ref);

  return (
    <FocusScope restoreFocus>
      <div
        {...mergeProps(overlayProps, dialogProps, otherProps, modalProps)}
        ref={ref}
        style={{
          background: 'white',
          color: 'black',
          padding: 30,
          ...style
        }}
      >
        <h3
          {...titleProps}
          style={{ marginTop: 0 }}
        >
          {title}
        </h3>
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
});

function Example() {
  let state = useOverlayTriggerState({});

  let triggerRef = React.useRef();
  let overlayRef = React.useRef();

  // Get props for the trigger and overlay. This also handles
  // hiding the overlay when a parent element of the trigger scrolls
  // (which invalidates the popover positioning).
  let { triggerProps, overlayProps } = useOverlayTrigger(
    { type: 'dialog' },
    state,
    triggerRef
  );

  // Get popover positioning props relative to the trigger
  let { overlayProps: positionProps } = useOverlayPosition({
    targetRef: triggerRef,
    overlayRef,
    placement: 'top',
    offset: 5,
    isOpen: state.isOpen
  });

  // useButton ensures that focus management is handled correctly,
  // across all browsers. Focus is restored to the button once the
  // popover closes.
  let { buttonProps } = useButton({
    onPress: () => state.open()
  }, triggerRef);

  return (
    <>
      <button
        {...buttonProps}
        {...triggerProps}
        ref={triggerRef}
      >
        Open Popover
      </button>
      {state.isOpen &&
        (
          <OverlayContainer>
            <Popover
              {...overlayProps}
              {...positionProps}
              ref={overlayRef}
              title="Popover title"
              isOpen={state.isOpen}
              onClose={state.close}
            >
              This is the content of the popover.
            </Popover>
          </OverlayContainer>
        )}
    </>
  );
}

// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
  <Example />
</OverlayProvider>
import {useOverlayTriggerState} from '@react-stately/overlays';
import {
  DismissButton,
  OverlayContainer,
  OverlayProvider,
  useModal,
  useOverlay,
  useOverlayPosition,
  useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';

const Popover = React.forwardRef(({
  title,
  children,
  isOpen,
  onClose,
  style,
  ...otherProps
}, ref) => {
  // Handle interacting outside the dialog and pressing
  // the Escape key to close the modal.
  let { overlayProps } = useOverlay({
    onClose,
    isOpen,
    isDismissable: true
  }, ref);

  // Hide content outside the modal from screen readers.
  let { modalProps } = useModal();

  // Get props for the dialog and its title
  let { dialogProps, titleProps } = useDialog({}, ref);

  return (
    <FocusScope restoreFocus>
      <div
        {...mergeProps(
          overlayProps,
          dialogProps,
          otherProps,
          modalProps
        )}
        ref={ref}
        style={{
          background: 'white',
          color: 'black',
          padding: 30,
          ...style
        }}
      >
        <h3
          {...titleProps}
          style={{ marginTop: 0 }}
        >
          {title}
        </h3>
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
});

function Example() {
  let state = useOverlayTriggerState({});

  let triggerRef = React.useRef();
  let overlayRef = React.useRef();

  // Get props for the trigger and overlay. This also handles
  // hiding the overlay when a parent element of the trigger scrolls
  // (which invalidates the popover positioning).
  let { triggerProps, overlayProps } = useOverlayTrigger(
    { type: 'dialog' },
    state,
    triggerRef
  );

  // Get popover positioning props relative to the trigger
  let { overlayProps: positionProps } = useOverlayPosition({
    targetRef: triggerRef,
    overlayRef,
    placement: 'top',
    offset: 5,
    isOpen: state.isOpen
  });

  // useButton ensures that focus management is handled correctly,
  // across all browsers. Focus is restored to the button once the
  // popover closes.
  let { buttonProps } = useButton({
    onPress: () => state.open()
  }, triggerRef);

  return (
    <>
      <button
        {...buttonProps}
        {...triggerProps}
        ref={triggerRef}
      >
        Open Popover
      </button>
      {state.isOpen &&
        (
          <OverlayContainer>
            <Popover
              {...overlayProps}
              {...positionProps}
              ref={overlayRef}
              title="Popover title"
              isOpen={state.isOpen}
              onClose={state.close}
            >
              This is the content of the popover.
            </Popover>
          </OverlayContainer>
        )}
    </>
  );
}

// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
  <Example />
</OverlayProvider>
import {useOverlayTriggerState} from '@react-stately/overlays';
import {
  DismissButton,
  OverlayContainer,
  OverlayProvider,
  useModal,
  useOverlay,
  useOverlayPosition,
  useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';

const Popover = React
  .forwardRef(({
    title,
    children,
    isOpen,
    onClose,
    style,
    ...otherProps
  }, ref) => {
    // Handle interacting outside the dialog and pressing
    // the Escape key to close the modal.
    let {
      overlayProps
    } = useOverlay({
      onClose,
      isOpen,
      isDismissable: true
    }, ref);

    // Hide content outside the modal from screen readers.
    let { modalProps } =
      useModal();

    // Get props for the dialog and its title
    let {
      dialogProps,
      titleProps
    } = useDialog(
      {},
      ref
    );

    return (
      <FocusScope
        restoreFocus
      >
        <div
          {...mergeProps(
            overlayProps,
            dialogProps,
            otherProps,
            modalProps
          )}
          ref={ref}
          style={{
            background:
              'white',
            color:
              'black',
            padding: 30,
            ...style
          }}
        >
          <h3
            {...titleProps}
            style={{
              marginTop:
                0
            }}
          >
            {title}
          </h3>
          {children}
          <DismissButton
            onDismiss={onClose}
          />
        </div>
      </FocusScope>
    );
  });

function Example() {
  let state =
    useOverlayTriggerState(
      {}
    );

  let triggerRef = React
    .useRef();
  let overlayRef = React
    .useRef();

  // Get props for the trigger and overlay. This also handles
  // hiding the overlay when a parent element of the trigger scrolls
  // (which invalidates the popover positioning).
  let {
    triggerProps,
    overlayProps
  } = useOverlayTrigger(
    { type: 'dialog' },
    state,
    triggerRef
  );

  // Get popover positioning props relative to the trigger
  let {
    overlayProps:
      positionProps
  } = useOverlayPosition(
    {
      targetRef:
        triggerRef,
      overlayRef,
      placement: 'top',
      offset: 5,
      isOpen:
        state.isOpen
    }
  );

  // useButton ensures that focus management is handled correctly,
  // across all browsers. Focus is restored to the button once the
  // popover closes.
  let { buttonProps } =
    useButton({
      onPress: () =>
        state.open()
    }, triggerRef);

  return (
    <>
      <button
        {...buttonProps}
        {...triggerProps}
        ref={triggerRef}
      >
        Open Popover
      </button>
      {state.isOpen &&
        (
          <OverlayContainer>
            <Popover
              {...overlayProps}
              {...positionProps}
              ref={overlayRef}
              title="Popover title"
              isOpen={state
                .isOpen}
              onClose={state
                .close}
            >
              This is the
              content of
              the
              popover.
            </Popover>
          </OverlayContainer>
        )}
    </>
  );
}

// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
  <Example />
</OverlayProvider>