Skip to content

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

OverlaysReact

Tooltip

A popup that appears when an element is hovered or focused, showing a hint for sighted users.

Rendered by React 19.3.1

import { Tooltip } from '@hamur/react/tooltip'

export default function TooltipExample() {
  return (
    <Tooltip.Provider>
      <Tooltip.Root>
        <Tooltip.Trigger className="button" aria-label="Bold">
          B
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Positioner sideOffset={8}>
            <Tooltip.Popup className="tooltip">
              <Tooltip.Arrow className="arrow" />
              Bold
            </Tooltip.Popup>
          </Tooltip.Positioner>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}
660Tests passing in React · 0 failing
41.3 KiBSize
1.90 msOpen
17.3 msClose
16.0 msOpenMutations
11.0 msCloseMutations
1.07 MiBMemory
productdevbook and 18 upstream authors11 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:

<Tooltip.Provider>
  <Tooltip.Root>
    <Tooltip.Trigger />
    <Tooltip.Portal>
      <Tooltip.Positioner>
        <Tooltip.Popup>
          <Tooltip.Arrow />
        </Tooltip.Popup>
      </Tooltip.Positioner>
    </Tooltip.Portal>
  </Tooltip.Root>
</Tooltip.Provider>

// Not in this example: Tooltip.Viewport

Examples

Detached Triggers Simple

A Trigger outside Root is tied to it with a handle.

TooltipDetachedTriggersSimple.tsx
import { Tooltip } from '@hamur/react/tooltip'

const demoTooltip = Tooltip.createHandle()

export default function TooltipDetachedTriggersSimpleExample() {
  return (
    <Tooltip.Provider>
      <Tooltip.Trigger className="icon-square" handle={demoTooltip} aria-label="Delete">
        <TrashIcon />
      </Tooltip.Trigger>

      <Tooltip.Root handle={demoTooltip}>
        <Tooltip.Portal>
          <Tooltip.Positioner sideOffset={11}>
            <Tooltip.Popup className="tooltip">
              <Tooltip.Arrow className="arrow" />
              Delete
            </Tooltip.Popup>
          </Tooltip.Positioner>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}

function TrashIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeLinejoin="round"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <path strokeLinecap="square" d="M2.5 4h11" />
      <path strokeLinecap="round" d="M6.5 4V3c0-.82843.67157-1.5 1.5-1.5s1.5.67157 1.5 1.5v1" />
      <path
        strokeLinecap="square"
        d="m3.5 4 .87069 9.1422c.07332.7699.7199 1.3578 1.49324 1.3578h4.27217c.7733 0 1.4199-.5879 1.4932-1.3578L12.5 4"
      />
    </svg>
  )
}

Detached Triggers Controlled

open, onOpenChange and triggerId control one tooltip from several triggers.

TooltipDetachedTriggersControlled.tsx
import * as React from 'react'
import { Tooltip } from '@hamur/react/tooltip'

const demoTooltip = Tooltip.createHandle()

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

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

  return (
    <Tooltip.Provider>
      <div className="row">
        <div className="icon-square-group">
          <Tooltip.Trigger
            className="icon-square"
            handle={demoTooltip}
            id="trigger-1"
            aria-label="Trigger 1"
          >
            <HeadphonesIcon />
          </Tooltip.Trigger>

          <Tooltip.Trigger
            className="icon-square"
            handle={demoTooltip}
            id="trigger-2"
            aria-label="Trigger 2"
          >
            <StopwatchIcon />
          </Tooltip.Trigger>

          <Tooltip.Trigger
            className="icon-square"
            handle={demoTooltip}
            id="trigger-3"
            aria-label="Trigger 3"
          >
            <TrashIcon />
          </Tooltip.Trigger>
        </div>

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

      <Tooltip.Root
        handle={demoTooltip}
        open={open}
        onOpenChange={handleOpenChange}
        triggerId={triggerId}
      >
        <Tooltip.Portal>
          <Tooltip.Positioner sideOffset={11}>
            <Tooltip.Popup className="tooltip">
              <Tooltip.Arrow className="arrow" />
              Controlled tooltip
            </Tooltip.Popup>
          </Tooltip.Positioner>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}

function HeadphonesIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <path strokeLinecap="round" d="M1.5 11V7.5c0-2.5 2.5-6 6.5-6s6.5 3.5 6.5 6V11" />
      <path d="M12 7.5c1.3807 0 2.5 1.11929 2.5 2.5v2c0 1.3807-1.1193 2.5-2.5 2.5h-1.5v-7zm-8 0h1.5v7H4c-1.38071 0-2.5-1.1193-2.5-2.5v-2c0-1.38071 1.11929-2.5 2.5-2.5Z" />
    </svg>
  )
}

function StopwatchIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <circle cx="8" cy="8.5" r="6" />
      <path
        strokeLinecap="square"
        strokeLinejoin="round"
        d="M8 9.5v-5m0-2v-2m-2 0h4M12 4l1.5-1.5"
      />
    </svg>
  )
}

function TrashIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeLinejoin="round"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <path strokeLinecap="square" d="M2.5 4h11" />
      <path strokeLinecap="round" d="M6.5 4V3c0-.82843.67157-1.5 1.5-1.5s1.5.67157 1.5 1.5v1" />
      <path
        strokeLinecap="square"
        d="m3.5 4 .87069 9.1422c.07332.7699.7199 1.3578 1.49324 1.3578h4.27217c.7733 0 1.4199-.5879 1.4932-1.3578L12.5 4"
      />
    </svg>
  )
}

Detached Triggers Full

Viewport animates the content as the tooltip moves between triggers.

TooltipDetachedTriggersFull.tsx
import { Tooltip } from '@hamur/react/tooltip'

const demoTooltip = Tooltip.createHandle<string>()

export default function TooltipDetachedTriggersFullExample() {
  return (
    <Tooltip.Provider>
      <div className="icon-square-group">
        <Tooltip.Trigger
          className="icon-square"
          handle={demoTooltip}
          payload="Listen to audio preview"
          aria-label="Listen to audio preview"
        >
          <HeadphonesIcon />
        </Tooltip.Trigger>

        <Tooltip.Trigger
          className="icon-square"
          handle={demoTooltip}
          payload="Set a timer"
          aria-label="Set a timer"
        >
          <StopwatchIcon />
        </Tooltip.Trigger>

        <Tooltip.Trigger
          className="icon-square"
          handle={demoTooltip}
          payload="Delete: This action cannot be undone"
          aria-label="Delete: This action cannot be undone"
        >
          <TrashIcon />
        </Tooltip.Trigger>
      </div>

      <Tooltip.Root handle={demoTooltip}>
        {({ payload }) => (
          <Tooltip.Portal>
            <Tooltip.Positioner sideOffset={11} className="popup-morph-positioner">
              <Tooltip.Popup className="tooltip popup-morph">
                <Tooltip.Arrow className="arrow" />

                <Tooltip.Viewport className="popup-morph-viewport">{payload}</Tooltip.Viewport>
              </Tooltip.Popup>
            </Tooltip.Positioner>
          </Tooltip.Portal>
        )}
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}

function HeadphonesIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <path strokeLinecap="round" d="M1.5 11V7.5c0-2.5 2.5-6 6.5-6s6.5 3.5 6.5 6V11" />
      <path d="M12 7.5c1.3807 0 2.5 1.11929 2.5 2.5v2c0 1.3807-1.1193 2.5-2.5 2.5h-1.5v-7zm-8 0h1.5v7H4c-1.38071 0-2.5-1.1193-2.5-2.5v-2c0-1.38071 1.11929-2.5 2.5-2.5Z" />
    </svg>
  )
}

function StopwatchIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <circle cx="8" cy="8.5" r="6" />
      <path
        strokeLinecap="square"
        strokeLinejoin="round"
        d="M8 9.5v-5m0-2v-2m-2 0h4M12 4l1.5-1.5"
      />
    </svg>
  )
}

function TrashIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeLinejoin="round"
      aria-hidden="true"
      style={{ display: 'block' }}
    >
      <path strokeLinecap="square" d="M2.5 4h11" />
      <path strokeLinecap="round" d="M6.5 4V3c0-.82843.67157-1.5 1.5-1.5s1.5.67157 1.5 1.5v1" />
      <path
        strokeLinecap="square"
        d="m3.5 4 .87069 9.1422c.07332.7699.7199 1.3578 1.49324 1.3578h4.27217c.7733 0 1.4199-.5879 1.4932-1.3578L12.5 4"
      />
    </svg>
  )
}

API reference

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

ArrowTooltip.Arrow

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

Arrow data attributes
Data attributeDescription
data-openPresent when the tooltip is open.
data-closedPresent when the tooltip 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 tooltip arrow is uncentered.
data-instantPresent if animations should be instant.

PopupTooltip.Popup

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

Popup data attributes
Data attributeDescription
data-openPresent when the tooltip is open.
data-closedPresent when the tooltip is closed.
data-starting-stylePresent when the tooltip begins animating in.
data-ending-stylePresent when the tooltip 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.

PortalTooltip.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

PositionerTooltip.Positioner

Positions the tooltip 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'top'
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 tooltip is open.
data-closedPresent when the tooltip 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 tooltip's positioner. It is important to set width to this value when using CSS to animate size changes.
--positioner-heightThe height of the tooltip's positioner. It is important to set height to this value when using CSS to animate size changes.

ProviderTooltip.Provider

Provides a shared delay for multiple tooltips. The grouping logic ensures that once a tooltip becomes visible, the adjacent tooltips will be shown instantly.

Provider props
PropTypeDefault
delayHow long to wait before opening the tooltip on hover.number—
closeDelayHow long to wait before closing a tooltip.number—
timeoutAnother tooltip will open instantly if the previous tooltipnumber400
childrenReact.ReactNode—

RootTooltip.Root

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

Root props
PropTypeDefault
defaultOpenWhether the tooltip is initially open.booleanfalse
openWhether the tooltip is currently open.boolean—
onOpenChangeEvent handler called when the tooltip is opened or closed.((open: boolean, eventDetails: TooltipRootChangeEventDetails) => void)—
actionsRefA ref to imperative actions.React.RefObject<TooltipRootActions | null>—
defaultTriggerIdID of the trigger that the tooltip is associated with.string | null—
handleA handle to associate the tooltip with a trigger.TooltipHandle<Payload>—
onOpenChangeCompleteEvent handler called after any animations complete when the tooltip is opened or closed.((open: boolean) => void)—
triggerIdID of the trigger that the tooltip is associated with.string | null—
trackCursorAxisDetermines which axis the tooltip should track the cursor on."none" | "both" | "x" | "y"'none'
disabledWhether the tooltip is disabled.booleanfalse
disableHoverablePopupWhether the tooltip contents can be hovered without closing the tooltip.booleanfalse
childrenThe content of the tooltip.React.ReactNode | PayloadChildRenderFunction<Payload>—

TriggerTooltip.Trigger

An element to attach the tooltip to. Renders a <button>.

Trigger props
PropTypeDefault
closeOnClickWhether the tooltip should close when this trigger is clicked.booleantrue
handleA handle to associate the trigger with a tooltip.TooltipHandle<Payload>—
payloadA payload to pass to the tooltip when it is opened.Payload—
disabledIf true, the tooltip will not open when interacting with this trigger.booleanfalse
delayHow long to wait before opening the tooltip on hover.number600
closeDelayHow long to wait before closing the tooltip.number0
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding tooltip is open.
data-trigger-disabledPresent when the trigger is disabled, either by the disabled prop or by a parent <Tooltip.Root> component.

ViewportTooltip.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
TabFocusing Trigger opens the tooltipTrigger
EscapeCloses the tooltipPopup

Accessibility

What the parts set
PartSetsSo that
Popupno role and no link to Triggerthe hint is not read twice; touch and screen reader users do not depend on it
Triggerneeds an aria-label that matches the tooltip's textevery user gets the same name for the control
Arrowaria-hidden="true"the decorative arrow is not read

Focus order

  1. Trigger is the tab stop; focus never moves into Popup.
  2. Focusing Trigger opens the tooltip, and leaving it or pressing Escape closes it.