hono-preact

Tooltip

A small label that appears on hover or keyboard focus. It follows WCAG 1.4.13: the tooltip is hoverable (you can move the pointer onto it), dismissible with Escape, and persistent (it stays until you leave or press Escape). Tooltips are not shown for touch input, which cannot hover; do not put essential information only in a tooltip. It ships unstyled: style it through the data-state, data-side, and data-align contract.

Demo

Usage

import { Tooltip } from '@hono-preact/ui';

export function SaveHint() {
  return (
    <Tooltip.Root>
      <Tooltip.Trigger>Hover me</Tooltip.Trigger>
      <Tooltip.Positioner>
        <Tooltip.Popup>
          <Tooltip.Arrow />
          Saved to your library
        </Tooltip.Popup>
      </Tooltip.Positioner>
    </Tooltip.Root>
  );
}

The trigger opens after a short delay on hover and immediately on focus. Set delay and closeDelay on Tooltip.Root to tune the timing.

Once open, the tooltip stays open while the pointer rests on the trigger, the popup, or the safe corridor across the gap between them; it closes closeDelay after the pointer leaves that region, and a move back inside cancels the close. See useSafeArea.

Styling

Parts expose data-state="open" | "closed"; the Positioner and Arrow also expose data-side and data-align. Size, z-index, and entry animation go on the Popup inside the Positioner. The demo above uses the styles below; copy a starting point in either flavor:

.docs-tooltip-positioner {
  z-index: 50;
}
.docs-tooltip {
  padding: 0.375rem 0.625rem;
  border-radius: 0.375rem;
  background: #18181b;
  color: #fafafa;
  font-size: 0.8125rem;
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.18);
  opacity: 1;
  transition: opacity 100ms ease;
}
@starting-style {
  .docs-tooltip[data-state='open'] {
    opacity: 0;
  }
}
.docs-tooltip[data-state='closed'] {
  animation: docs-tooltip-out 100ms ease-in forwards;
}
@keyframes docs-tooltip-out {
  to {
    opacity: 0;
  }
}
/* A solid rotated square, same color as the chip, straddling the edge facing
   the anchor. */
.docs-tooltip__arrow {
  width: 8px;
  height: 8px;
  rotate: 45deg;
  background: #18181b;
}
.docs-tooltip__arrow[data-side='bottom'] {
  top: -4px;
}
.docs-tooltip__arrow[data-side='top'] {
  bottom: -4px;
}
.docs-tooltip__arrow[data-side='right'] {
  left: -4px;
}
.docs-tooltip__arrow[data-side='left'] {
  right: -4px;
}
@media (prefers-color-scheme: dark) {
  .docs-tooltip {
    background: #27272a;
    color: #fafafa;
  }
  .docs-tooltip__arrow {
    background: #27272a;
  }
}
@media (prefers-reduced-motion: reduce) {
  .docs-tooltip {
    transition: none;
  }
}

API reference

Every part accepts a render prop for composition (see useRender) and forwards unknown props to the element it renders. The Trigger, Positioner, Popup, and Arrow also expose data-state="open" | "closed" and pass state to a render function.

Tooltip.Root

Provides open state, ids, refs, timing, and placement config to the parts. Renders only its children.

PropTypeDefaultDescription
openboolean-Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the tooltip requests an open or close.
delaynumber600Open delay in ms after the pointer enters the trigger.
closeDelaynumber300Grace in ms before closing once the pointer leaves the trigger, popup, or safe corridor.
side'top'|'right'|'bottom'|'left''top'Preferred side of the anchor to place the popup.
align'start'|'center'|'end''center'Alignment along that side.
offsetnumber8Gap in pixels between the anchor and the popup.
childrenComponentChildren-The trigger and positioner.

Tooltip.Trigger

The element the tooltip describes. Binds hover and focus, wires aria-describedby, and anchors positioning. Default element <button type="button">; use render to attach it to your own control.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Trigger content.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded; passed pointer / focus handlers run first.

Sets data-state and aria-describedby (the popup id, only while open). Opens after delay on a mouse pointerenter, immediately on focus; once open, it closes closeDelay after the pointer leaves the trigger, popup, and safe corridor, immediately on blur, or on Escape. A touch pointer does not open it.

Tooltip.Positioner

The fixed-positioned wrapper that Floating UI drives. Renders nothing until the tooltip is open (mount on open). Default element <div>.

PropTypeDefaultDescription
renderRenderProp<{ side: Side; align: Align }>-Compose or replace the element.
childrenComponentChildren-The popup (and optional arrow).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element. Style positioning via class.

Sets position: fixed and the resolved data-side / data-align. The element is promoted to the top layer via the Popover API, so it escapes ancestor clipping.

Tooltip.Popup

The tooltip surface. Default element <div> with role="tooltip".

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Tooltip content (and optional arrow).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets role="tooltip", id, and data-state. Hoverable: moving the pointer onto the popup cancels the close so the content stays readable.

Tooltip.Arrow

Optional pointer positioned from the Floating UI arrow data. Default element <div>. Place it on the correct edge with CSS keyed on data-side.

PropTypeDefaultDescription
renderRenderProp<{ side: Side }>-Compose or replace the element.
childrenComponentChildren-Optional arrow content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets data-side and position: absolute with the computed offset.

Primitives

@hono-preact/ui exports the building blocks the parts use, for composing your own components. Several have their own page with examples: usePosition, useDismiss, useRender, useControllableState, mergeRefs, and useSafeArea.

Accessibility

The tooltip is wired to its trigger through aria-describedby while open, so a screen reader announces the content when the trigger receives focus. The popup has role="tooltip".

  • Both hover and keyboard focus open the tooltip, so it is reachable without a pointer.
  • It is hoverable: moving the pointer from the trigger onto the popup keeps it open (WCAG 1.4.13), so content that needs to be read or selected does not vanish.
  • It is dismissible with Escape and persistent: while the pointer rests on the trigger, the popup, or the safe corridor between them it never auto-hides on a timer; it closes once focus or the pointer leaves.
  • Touch input cannot hover, so the tooltip is suppressed there. Do not place information that is only available through a tooltip; keep it supplementary.