Detent web components
By Shane Logsdon

Overlays & menus

Tooltip

Implemented

Appears from its origin direction

detent-tooltip

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-overlays
Group
Overlays & menus
Top Appears from above, centred

5.5 KB gzip for the class module · inlines the shared spring

Variants

Right Appears from the right, aligned to the start edge Bottom Appears from below after a 300 ms delay Left Appears from the left, 16 px away Force open Force-open previews the settled tooltip

Recorded states

  • hover recorded state; no setter
  • open recorded state; no setter
  • dark theme recorded state; no setter

Usage

Import

import '/components/tooltip/define.js';

// or register it yourself
import { DetentTooltip } from '/components/tooltip/tooltip.js';

Attributes

  • align
  • delay-duration
  • force-open
  • offset
  • show-arrow
  • side

Properties

No properties are recorded for this component.

Slots

  • content
  • default

Events

No events are recorded for this component.

Example

Runnable example: site/examples/tooltip.ts. The snippet below is generated from the catalogued attributes.

<detent-tooltip
  align="center"
  delay-duration="150"
  force-open
  offset="8"
  show-arrow
  side="top"
></detent-tooltip>

Source

The pinned upstream source for this row:

  • src/components/ui/DirectionalTooltip.tsx sha256 bcce99ca9496…

Native files

The native module and its artifacts:

  • src/components/tooltip/tooltip.ts
  • src/components/tooltip/styles.css
  • src/components/tooltip/define.ts
  • docs/components/tooltip.md
  • site/examples/tooltip.ts
  • tests/browser/tooltip.spec.ts
  • evidence/components/tooltip.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
alignalign'start' | 'center' | 'end''center'Kebab-case attribute; camelCase property.
delay-durationdelayDurationnumber150Kebab-case attribute; camelCase property.
force-openforceOpenbooleanfalsePresence semantics: present means true, absent means false.
offsetoffsetnumber8Kebab-case attribute; camelCase property.
show-arrowshowArrowbooleantruePresence semantics: present means true, absent means false.
sideside'top' | 'right' | 'bottom' | 'left''top'Kebab-case attribute; camelCase property.

Properties

No property-only values are recorded for this component.

Events

No custom events are recorded for this component.

Slots

Slots
SlotNotes
contentConsumer content; rendered through a slot, never re-created.
defaultConsumer content; rendered through a slot, never re-created.

CSS customisation

  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The trigger is never cloned. Upstream React.cloneElementd the child to inject refs and handlers; this port listens on the host and leaves the consumer's node, listeners and focus untouched (PC-11.2/11.6). children/className are the default slot and documented parts/custom properties.
  • React nodes become slots. content is slot="content"; content is text or consumer DOM, never parsed markup (PC-11.1).
  • A manual popover replaces position: absolute. The tooltip is in the top layer, so ancestor overflow/transform cannot clip or offset it (PC-9.8); the upstream absolute placement could be clipped.
  • Placement is computed and clamped. Upstream only applied the preferred side and alignment (its "may be flipped" note was not implemented) and its centred position could overflow the viewport; this port flips to the opposite side when the preferred one has no room and clamps to an 8 px margin.
  • The arrow is a clipped caret in the muted text token instead of a border-4 triangle drawn with borderTopColor: var(--border); it is decorative and aria-hidden (PC-11.5).
  • Description wiring is implemented. Upstream set aria-describedby to a useId inside the same React tree; a shadow-DOM id cannot be referenced from the document, so the slotted content element (or aria-description for text-only content) is the description target, and consumer values are restored on close.
  • Escape dismisses the tooltip (WCAG 1.4.13). Upstream had no dismissal path at all; only the most recently opened tooltip handles the key so stacked tooltips do not all close at once.
  • The spring is the shared helper with the pinned springSnappy values; the exit animation is dropped (the tooltip hides immediately so the pointer target cannot linger), and reduced motion writes the settled state with no frames (PC-10.2/10.5).
  • white-space: nowrap became wrapping text with max-width: 20rem and overflow-wrap: break-word, so long tooltips stay inside the viewport and remain readable at narrow widths (PC-9.6).

What it does

A tooltip that opens toward the side with room, reached by hover or keyboard focus and rendered above everything so it is never clipped.

Upstream behaviour
  • Entry motion originates from the chosen side (top/right/bottom/left)
  • Snappy spring (springSnappy) settles the tooltip into place
  • Alignment per side: start / center / end
  • Configurable delay, offset, and optional arrow indicator
  • Light/dark theme aware via CSS variables
Differences from upstream
  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The trigger is never cloned. Upstream React.cloneElementd the child to inject refs and handlers; this port listens on the host and leaves the consumer's node, listeners and focus untouched (PC-11.2/11.6). children/className are the default slot and documented parts/custom properties.
  • React nodes become slots. content is slot="content"; content is text or consumer DOM, never parsed markup (PC-11.1).
  • A manual popover replaces position: absolute. The tooltip is in the top layer, so ancestor overflow/transform cannot clip or offset it (PC-9.8); the upstream absolute placement could be clipped.
  • Placement is computed and clamped. Upstream only applied the preferred side and alignment (its "may be flipped" note was not implemented) and its centred position could overflow the viewport; this port flips to the opposite side when the preferred one has no room and clamps to an 8 px margin.
  • The arrow is a clipped caret in the muted text token instead of a border-4 triangle drawn with borderTopColor: var(--border); it is decorative and aria-hidden (PC-11.5).
  • Description wiring is implemented. Upstream set aria-describedby to a useId inside the same React tree; a shadow-DOM id cannot be referenced from the document, so the slotted content element (or aria-description for text-only content) is the description target, and consumer values are restored on close.
  • Escape dismisses the tooltip (WCAG 1.4.13). Upstream had no dismissal path at all; only the most recently opened tooltip handles the key so stacked tooltips do not all close at once.
  • The spring is the shared helper with the pinned springSnappy values; the exit animation is dropped (the tooltip hides immediately so the pointer target cannot linger), and reduced motion writes the settled state with no frames (PC-10.2/10.5).
  • white-space: nowrap became wrapping text with max-width: 20rem and overflow-wrap: break-word, so long tooltips stay inside the viewport and remain readable at narrow widths (PC-9.6).
Previous: Depth Corridor Next: Donut Chart

Docs

Components

Project