Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

OverlaysReact

Popover

An accessible popup anchored to a button.

Rendered by React 19.3.1

Loading React…
import { Popover } from '@hamur/react/popover'

export default function PopoverExample() {
  return (
    <Popover.Root>
      <Popover.Trigger className="button">
        Notifications
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner sideOffset={8}>
          <Popover.Popup className="popover">
            <Popover.Arrow className="arrow" />
            <Popover.Title className="popover-title">
              Notifications
            </Popover.Title>
            <Popover.Description className="muted">
              You are all caught up.
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  )
}
996Tests passing in React · 0 failing
48.8 KiBSize
2.10 msOpen
17.4 msClose
25.0 msOpenMutations
20.0 msCloseMutations
1.03 MiBMemory
productdevbook and 22 upstream authors17 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Popover.Root>
  <Popover.Trigger />
  <Popover.Portal>
    <Popover.Positioner>
      <Popover.Popup>
        <Popover.Arrow />
        <Popover.Title />
        <Popover.Description />
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

// Not in this example: Popover.Backdrop, Popover.Close, Popover.Viewport

Examples

Open On Hover

openOnHover and delay open the popover when the pointer rests on the trigger.

PopoverOpenOnHover.tsx
import { Popover } from '@hamur/react/popover'

export default function PopoverOpenOnHoverExample() {
  return (
    <Popover.Root>
      <Popover.Trigger openOnHover className="button">
        Notifications
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner sideOffset={8}>
          <Popover.Popup className="popover">
            <Popover.Arrow className="arrow" />
            <Popover.Title className="popover-title">Notifications</Popover.Title>
            <Popover.Description className="muted">
              You are all caught up. Good job!
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  )
}

Detached Triggers Simple

A trigger outside Root opens it through a handle.

PopoverDetachedTriggersSimple.tsx
import { Popover } from '@hamur/react/popover'

const demoPopover = Popover.createHandle()

export default function PopoverDetachedTriggersSimpleExample() {
  return (
    <>
      <Popover.Trigger className="button" handle={demoPopover}>
        Notifications
      </Popover.Trigger>

      <Popover.Root handle={demoPopover}>
        <Popover.Portal>
          <Popover.Positioner sideOffset={8}>
            <Popover.Popup className="popover">
              <Popover.Arrow className="arrow" />
              <Popover.Title className="popover-title">Notifications</Popover.Title>
              <Popover.Description className="muted">
                You are all caught up. Good job!
              </Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </>
  )
}

Detached Triggers Controlled

open with triggerId lets several triggers share one popover.

PopoverDetachedTriggersControlled.tsx
import * as React from 'react'
import { Popover } from '@hamur/react/popover'

const demoPopover = Popover.createHandle()

export default function PopoverDetachedTriggersControlledExample() {
  const [open, setOpen] = React.useState(false)
  const [triggerId, setTriggerId] = React.useState<string | null>(null)

  const handleOpenChange = (isOpen: boolean, eventDetails: Popover.Root.ChangeEventDetails) => {
    setOpen(isOpen)
    setTriggerId(eventDetails.trigger?.id ?? null)
  }

  return (
    <>
      <div className="row">
        <Popover.Trigger className="button" handle={demoPopover} id="trigger-1">
          Trigger 1
        </Popover.Trigger>

        <Popover.Trigger className="button" handle={demoPopover} id="trigger-2">
          Trigger 2
        </Popover.Trigger>

        <Popover.Trigger className="button" handle={demoPopover} id="trigger-3">
          Trigger 3
        </Popover.Trigger>

        <button
          type="button"
          className="button"
          onClick={() => {
            setTriggerId('trigger-2')
            setOpen(true)
          }}
        >
          Open programmatically
        </button>
      </div>

      <Popover.Root
        handle={demoPopover}
        open={open}
        onOpenChange={handleOpenChange}
        triggerId={triggerId}
      >
        <Popover.Portal>
          <Popover.Positioner sideOffset={8}>
            <Popover.Popup className="popover">
              <Popover.Arrow className="arrow" />
              <Popover.Title className="popover-title">Notifications</Popover.Title>
              <Popover.Description className="muted">
                You are all caught up. Good job!
              </Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </>
  )
}

Detached Triggers Full

Viewport animates the content when another trigger opens the same popover.

PopoverDetachedTriggersFull.tsx
import { Popover } from '@hamur/react/popover'

const demoPopover = Popover.createHandle<string>()

// An inline image keeps the example off the network; use your own URL in an app.
const avatar =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' fill='%232457d6'/%3E%3Ccircle cx='24' cy='19' r='8' fill='white'/%3E%3Cpath d='M8 48a16 16 0 0 1 32 0' fill='white'/%3E%3C/svg%3E"

export default function PopoverDetachedTriggersFullExample() {
  return (
    <div className="row">
      <Popover.Trigger className="button" handle={demoPopover} payload="notifications">
        Notifications
      </Popover.Trigger>

      <Popover.Trigger className="button" handle={demoPopover} payload="activity">
        Activity
      </Popover.Trigger>

      <Popover.Trigger className="button" handle={demoPopover} payload="profile">
        Profile
      </Popover.Trigger>

      <Popover.Root handle={demoPopover}>
        {({ payload }) => (
          <Popover.Portal>
            <Popover.Positioner sideOffset={8} className="popup-morph-positioner">
              <Popover.Popup className="popover popup-morph">
                <Popover.Arrow className="arrow" />

                <Popover.Viewport className="popup-morph-viewport">
                  {payload === 'notifications' && <NotificationsPanel />}
                  {payload === 'activity' && <ActivityPanel />}
                  {payload === 'profile' && <ProfilePanel />}
                </Popover.Viewport>
              </Popover.Popup>
            </Popover.Positioner>
          </Popover.Portal>
        )}
      </Popover.Root>
    </div>
  )
}

function NotificationsPanel() {
  return (
    <div className="popover-panel">
      <Popover.Title className="popover-title">Notifications</Popover.Title>
      <Popover.Description className="muted">You are all caught up. Good job!</Popover.Description>
    </div>
  )
}

function ProfilePanel() {
  return (
    <div className="popover-profile">
      <Popover.Title className="popover-title">Jason Eventon</Popover.Title>
      <img src={avatar} width="48" height="48" alt="" className="popover-profile-avatar" />
      <span className="muted">Pro plan</span>
      <div className="popover-profile-actions">
        <a href="#top">Profile settings</a>
        <a href="#top">Log out</a>
      </div>
    </div>
  )
}

function ActivityPanel() {
  return (
    <div className="popover-panel">
      <Popover.Title className="popover-title">Activity</Popover.Title>
      <Popover.Description className="muted">
        Nothing interesting happened recently.
      </Popover.Description>
    </div>
  )
}

API reference

35 props across 11 parts, in React syntax. className, style, render are listed once, under Shared props.

ArrowPopover.Arrow

Displays an element positioned against the popover anchor. Renders a <div>.

Arrow data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-uncenteredPresent when the popover arrow is uncentered.

BackdropPopover.Backdrop

An overlay displayed beneath the popover. Renders a <div>.

Backdrop data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-starting-stylePresent when the popup begins animating in.
data-ending-stylePresent when the popup is animating out.

ClosePopover.Close

A button that closes the popover. Renders a <button>.

Close props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

DescriptionPopover.Description

A paragraph with additional information about the popover. Renders a <p>.

PopupPopover.Popup

A container for the popover contents. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the popover is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the popover is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
Popup data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-starting-stylePresent when the popup begins animating in.
data-ending-stylePresent when the popup is animating out.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-instantPresent if animations should be instant.
Popup CSS variables
CSS variableDescription
--popup-widthThe width of the popup.
--popup-heightThe height of the popup.

PortalPopover.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse

PositionerPopover.Positioner

Positions the popover against the trigger. Renders a <div>.

Positioner props
PropTypeDefault
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
Positioner data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the trigger and the edge of the viewport.
--available-heightThe available height between the trigger and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.
--positioner-widthThe width of the popover's positioner. It is important to set width to this value when using CSS to animate size changes.
--positioner-heightThe height of the popover's positioner. It is important to set height to this value when using CSS to animate size changes.

RootPopover.Root

Groups all parts of the popover. Doesn’t render its own element.

Root props
PropTypeDefault
defaultOpenWhether the popover is initially open.booleanfalse
openWhether the popover is currently open.boolean—
onOpenChangeEvent handler called when the popover is opened or closed.((open: boolean, eventDetails: PopoverRootChangeEventDetails) => void)—
actionsRefA ref to imperative actions.React.RefObject<PopoverRootActions | null>—
defaultTriggerIdID of the trigger that the popover is associated with.string | null—
handleA handle to associate the popover with a trigger.PopoverHandle<Payload>—
modalDetermines if the popover enters a modal state when open.boolean | "trap-focus"false
onOpenChangeCompleteEvent handler called after any animations complete when the popover is opened or closed.((open: boolean) => void)—
triggerIdID of the trigger that the popover is associated with.string | null—
childrenThe content of the popover.React.ReactNode | PayloadChildRenderFunction<Payload>—

TitlePopover.Title

A heading that labels the popover. Renders a <h2>.

TriggerPopover.Trigger

A button that opens the popover. Renders a <button>.

Trigger props
PropTypeDefault
handleA handle to associate the trigger with a popover.PopoverHandle<Payload>—
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
payloadA payload to pass to the popover when it is opened.Payload—
openOnHoverWhether the popover should also open when the trigger is hovered.booleanfalse
delayHow long to wait before the popover may be opened on hover.number300
closeDelayHow long to wait before closing the popover that was opened on hover.number0
idID of the trigger.string—
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popover is open.
data-pressedPresent when the trigger is pressed.
data-disabledPresent when the trigger is disabled.

ViewportPopover.Viewport

A viewport for displaying content transitions. This component is only required if one popup can be opened by multiple triggers, its content changes based on the trigger, and switching between them is animated. Renders a <div>.

Viewport props
PropTypeDefault
childrenThe content to render inside the transition container.React.ReactNode—
Viewport data attributes
Data attributeDescription
data-currentApplied to the direct child of the viewport when no transitions are present or the new content when it's entering.
data-previousApplied to the direct child of the viewport that contains the exiting content when transitions are present.
data-activation-directionIndicates the direction from which the popup was activated. This can be used to create directional animations based on how the popup was triggered. Contains space-separated values for both horizontal and vertical axes.
data-transitioningIndicates that the viewport is currently transitioning between old and new content.
data-instantPresent if animations should be instant.
Viewport CSS variables
CSS variableDescription
--popup-widthThe width of the parent popup. This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.
--popup-heightThe height of the parent popup. This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
EnterSpaceOpens and closes the popover, also when Trigger renders an element other than <button>Trigger
EscapeCloses the popoverPopup
TabFrom the last element in the popup, moves on to the element after the triggerPopup
ShiftTabFrom the popup, moves back to the triggerPopup

Accessibility

What the parts set
PartSetsSo that
Triggeraria-haspopup="dialog", aria-expanded, aria-controlsa screen reader says the button opens a popup and whether it is open
Popuprole="dialog", aria-labelledby to Title, aria-describedby to Descriptionthe popup is announced with its heading and description
Popupsits right after the trigger in the tab order, wherever it is portaledkeyboard users reach the content from the button that opened it
Arrowaria-hidden="true"the decorative arrow is not read
Backdroprole="presentation"the overlay is not read

Focus order

  1. Opening with a click or a key moves focus into Popup, or to initialFocus; opening on hover leaves focus where it was.
  2. Tab moves through the popup and then to the element after Trigger; Shift+Tab goes back to Trigger.
  3. Closing returns focus to Trigger, or to finalFocus.