PixelTooltip

Floating-UI-positioned tooltip that anchors a portal-rendered hint to a trigger, with hover/focus/click activation, controlled or uncontrolled open state, and configurable open/close delays.

  • Auto-flip and shift via floating-ui — stays inside the viewport across all four positions.
  • Three trigger modes (hover, focus, click) — click variant accepts pointer events and dismisses on outside click or Escape.
  • Portal-rendered so it escapes overflow/transform ancestors without z-index gymnastics.
  • Controlled — open + onOpenChange (React), v-model:open (Vue), [(open)] (Angular) — or uncontrolled (defaultOpen).
  • Surface-aware (pixel/linear) and inherits from PxlKitSurfaceProvider when no surface prop is passed.
Status
stable
Category
overlays
Since
v1.0.0

API

import { PixelTooltip } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
contentReact.ReactNode—Tooltip body. Accepts any ReactNode; falls back to PixelTooltipProps.label when omitted.
labelstring—Backwards-compat string alias for PixelTooltipProps.content.
children (required)React.ReactNode—The element the tooltip is anchored to.
position'top' | 'bottom' | 'left' | 'right''top'Preferred placement. floating-ui will flip/shift away from viewport edges.
surface'pixel' | 'linear'—Visual surface override. Falls back to nearest <PxlKitProvider> surface.
delaynumber | { open?: number; close?: number }—Open/close delays in ms. A bare number is treated as { open } for backwards-compat with the previous API. Defaults to { open: 200, close: 100 }.
openboolean—Controlled open state. When provided, the tooltip ignores its internal state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void—Called whenever the tooltip wants to change open state (controlled or uncontrolled).
trigger'hover' | 'click' | 'focus''hover'How the tooltip opens. Default 'hover'.
sideOffsetnumber8Distance in px from the trigger. Default 8.

ref points to <span>.

Accessibility

WCAG target: 2.1 AA

  • tooltip

Floating panel has role="tooltip". While it is open, its id joins the aria-describedby of the first focusable element inside the anchor — the element a screen reader announces on focus — next to that element's own references; with nothing focusable inside, the wrapper carries it. Hover and focus triggers render a non-interactive panel (pointer-events: none) so the cursor stays on the anchor. Click triggers listen for clicks bubbling from the anchor child — the wrapper itself stays non-interactive so an interactive child (the common case) is not nested inside another control; anchor click tooltips to a button or link for keyboard support. Escape dismisses the tooltip in every trigger mode without moving the pointer or focus (WCAG 1.4.13); outside pointerdown also closes a click tooltip. Use focus or click trigger when the tooltip must be reachable without a pointer device.

Keyboard

KeyActionWhen
TabMoves focus to the trigger; hover and focus triggers open the tooltip on focus—
EnterToggles the tooltiptrigger="click" and the anchor child is interactive (e.g. a button)
SpaceToggles the tooltiptrigger="click" and the anchor child is interactive (e.g. a button)
EscapeCloses the tooltip, or cancels one about to open; with the hover and focus triggers it then stays closed until the pointer leaves and re-enters or focus leaves and returnsopen or opening, any trigger

Usage

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Default() {
  return (
    <PixelTooltip label="Save your changes">
      <PixelButton>Save</PixelButton>
    </PixelTooltip>
  );
}

Examples

Default

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Default() {
  return (
    <PixelTooltip label="Save your changes">
      <PixelButton>Save</PixelButton>
    </PixelTooltip>
  );
}

Positions

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Positions() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <PixelTooltip label="Top tooltip" position="top">
        <PixelButton>Top</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Bottom tooltip" position="bottom">
        <PixelButton>Bottom</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Left tooltip" position="left">
        <PixelButton>Left</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Right tooltip" position="right">
        <PixelButton>Right</PixelButton>
      </PixelTooltip>
    </div>
  );
}

Triggers

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Triggers() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <PixelTooltip label="Opens on hover or focus" trigger="hover">
        <PixelButton>Hover</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Opens on focus only" trigger="focus">
        <PixelButton>Focus</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Click to toggle, Escape to close" trigger="click">
        <PixelButton>Click</PixelButton>
      </PixelTooltip>
    </div>
  );
}

Surfaces

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Surfaces() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <PixelTooltip label="Pixel surface" surface="pixel">
        <PixelButton surface="pixel">Pixel</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Linear surface" surface="linear">
        <PixelButton surface="linear">Linear</PixelButton>
      </PixelTooltip>
    </div>
  );
}

Rich content

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function RichContent() {
  return (
    <PixelTooltip
      content={
        <span className="flex flex-col gap-0.5">
          <span className="font-semibold">Keyboard shortcut</span>
          <span className="opacity-80">Press ⌘K to open the command palette</span>
        </span>
      }
    >
      <PixelButton>Hover for details</PixelButton>
    </PixelTooltip>
  );
}

Custom delay

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function CustomDelay() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <PixelTooltip label="Opens instantly" delay={{ open: 0, close: 0 }}>
        <PixelButton>Instant</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Slow open, fast close" delay={{ open: 600, close: 0 }}>
        <PixelButton>Slow open</PixelButton>
      </PixelTooltip>
    </div>
  );
}

Controlled

import { useState } from 'react';
import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Controlled() {
  const [open, setOpen] = useState(false);
  return (
    <div className="flex items-center gap-4">
      <PixelTooltip open={open} onOpenChange={setOpen} label="Controlled tooltip">
        <PixelButton>Anchor</PixelButton>
      </PixelTooltip>
      <PixelButton tone="cyan" variant="outline" onClick={() => setOpen((v) => !v)}>
        {open ? 'Hide' : 'Show'}
      </PixelButton>
    </div>
  );
}

Uncontrolled

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function Uncontrolled() {
  return (
    <PixelTooltip defaultOpen label="Open by default" trigger="click">
      <PixelButton>Click to toggle</PixelButton>
    </PixelTooltip>
  );
}

Side offset

import { PixelTooltip, PixelButton } from '@pxlkit/ui-kit';

export function SideOffset() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <PixelTooltip label="Tight (2px)" sideOffset={2}>
        <PixelButton>Tight</PixelButton>
      </PixelTooltip>
      <PixelTooltip label="Roomy (16px)" sideOffset={16}>
        <PixelButton>Roomy</PixelButton>
      </PixelTooltip>
    </div>
  );
}

Related