react-tooltip-contemporary
v0.1.2
Published
contemporary react tooltip — CSS clip-path shape, native Popover API, CSS anchor positioning
Maintainers
Readme

Contemporary React Tooltip

✨ Demo
A small 8kB gzipped and no dependency React tooltip built on modern web features:
- CSS anchor positioning: the bubble pins itself to its trigger with
anchor-name/position-anchor/anchor(); no JS measuring on scroll. Old browsers degrade gracefully totitletooltip, no polyfill, no extra dependency. - Popover API: the bubble lives in browser's top layer, it escapes
overflow: hiddenandz-indexstacking with no portal. - Pure CSS shape: the rounded bubble and its arrow are one
clip-path: polygon(...), no borders, no pseudo-elements or SVG. - Zero-config styling: each component injects its own stylesheet slice at runtime; no CSS import and no bundler CSS loader required.
| Mode | Markup |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Wrapping (default) | <Tooltip content="Saved"> <button>Save</button></Tooltip> |
| External by ref | <button ref={ref}>Save</button><Tooltip anchorRef={ref} content="Saved" /> |
| External by name | <button style={{ anchorName: '--s' }}>Save</button><Tooltip anchorName="--s" content="Saved" open={open} onOpenChange={setOpen} /> |
Components
Each component injects its own stylesheet slice at runtime, so all three work standalone with no CSS import.
| Export | Role |
| --------------- | -------------------------------------------- |
| Tooltip | Behaviour, triggers, positioning. |
| TooltipShape | The bubble, e.g the clip-path shape + arrow. |
| TooltipAnchor | The anchor part of CSS anchor positioning. |
Tooltip props
| Prop | Type | Default | Notes |
| ---------------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------ |
| children | ReactNode | – | The trigger element (wrapping mode). |
| content | ReactNode | – | The bubble content. |
| placement | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Preferred side of the anchor. |
| arrowPlacement | 'start' \| 'center' \| 'end' | 'center' | Which way the bubble extends; arrow stays on the anchor centre (see below). |
| trigger | ('hover' \| 'focus' \| 'click')[] | ['hover', 'focus'] | Interactions that reveal the tooltip. |
| delayShow | number | 200 | ms before showing (hover/focus only; click is instant). |
| delayHide | number | 100 | ms before hiding (hover/focus only; click is instant). |
| offset | string | '0.25em' | Gap between anchor and bubble (any CSS length). |
| autoFlip | boolean | true | Flip to the opposite side when it would overflow. |
| defaultOpen | boolean | false | Initial open state (uncontrolled). |
| open | boolean | – | Controlled open state; pair with onOpenChange. |
| onOpenChange | (open: boolean) => void | – | Fires when the open state should change. |
| bubbleStyle | TooltipBubbleStyle | – | Bubble appearance (see below). |
| className | string | – | Applied to the popover element. |
| style | CSSProperties | – | Applied to the popover element. |
| anchorRef | RefObject<HTMLElement> | – | Attach to an existing element instead of wrapping children. See External anchor below. |
| anchorName | string | – | Use this CSS anchor name verbatim. See External anchor below. |
arrowPlacement
The arrow always points at the anchor's centre, arrowPlacement only
chooses which way the bubble body extends from it. 'center' (default) centres
the bubble on the anchor; 'start' keeps the arrow near the bubble's leading
edge so the body grows toward the trailing side; 'end' mirrors that. Handy
when the anchor sits near a viewport edge and you want the bubble to grow the
other way. The axis follows placement: left→right for top/bottom,
top→bottom for left/right.
<Tooltip content="Aligned to the start" placement="top" arrowPlacement="start">
<button>Hover me</button>
</Tooltip>bubbleStyle
Per-instance look of the bubble. Pass any subset; omitted fields fall back to
the library defaults. Most fields are applied as CSS custom properties on the
bubble. The one exception is cornerSegments, which selects a .corners-N
class.
| Field | Type | Default | Notes |
| -------------------- | ------------- | ------------ | --------------------------------------------------------- |
| background | string | '#000' | Bubble background (any CSS background). |
| color | string | '#fff' | Text color. |
| fontSize | string | '0.875rem' | Bubble font size. |
| radius | string | '0.5rem' | Corner radius (any CSS length). |
| arrowSize | string | '0.5rem' | Arrow size (half-diagonal). |
| paddingX | string | '0.7rem' | Horizontal padding. |
| paddingY | string | '0.4rem' | Vertical padding. |
| maxWidth | string | '16rem' | Maximum bubble width. |
| transitionDuration | string | '0.2s' | Fade in/out (and flip) duration. |
| cornerSegments | 3 \| 5 \| 7 | 5 | Straight segments per rounded corner, higher is smoother. |
<Tooltip
content="Custom bubble"
bubbleStyle={{
background: '#2563eb',
color: '#fff',
radius: '0.8rem',
arrowSize: '0.6rem',
paddingX: '1rem',
paddingY: '0.5rem',
maxWidth: '20rem',
transitionDuration: '0.25s',
cornerSegments: 7,
}}
>
<button>Hover me</button>
</Tooltip>Controlled usage
const [open, setOpen] = useState(false);
<Tooltip open={open} onOpenChange={setOpen} content="Controlled">
<button>Anchor</button>
</Tooltip>;External anchor (skip the wrapper)
When you'd rather attach the tooltip to an element you already render.
Without Tooltip wrapping it in an extra <div>. Pass anchorRef and
omit children. Tooltip writes anchor-name onto the referenced
element, wires the configured triggers to it, and mirrors
aria-describedby on it for accessibility:
const btnRef = useRef<HTMLButtonElement>(null);
<>
<button ref={btnRef}>Save</button>
<Tooltip anchorRef={btnRef} content="Saved" />
</>;If you'd rather own the CSS anchor name yourself, use anchorName. In
this mode Tooltip has no handle to your element, so it cannot wire
triggers; pair with controlled open / onOpenChange:
const [open, setOpen] = useState(false);
<>
<button
style={{ anchorName: '--save-btn' } as CSSProperties}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
Save
</button>
<Tooltip
anchorName="--save-btn"
open={open}
onOpenChange={setOpen}
content="Saved"
/>
</>;Browser support
Where CSS anchor positioning is missing, there is no polyfill and no extra dependency.
The styled bubble is skipped and string content is surfaced through the
element's native title tooltip instead.
Development
npm install
npm run dev # Storybook + css-to-js watcher
npm run build # type-check, build to lib/, generate + copy CSS
npm testEach component owns a *.css file; css-to-js regenerates the matching
*.css.generated.js, which the component injects at runtime.
License
MIT © Sergey Yakunin
