Portals and floating content
Popover, tooltip, preview card, positioning, backdrop and explicit portal destinations.
Quick start
After the application setup, copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.
'use client';
import { Popover } from '@fayegram-ai/faye-ui/overlays/popover';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
export function PlaceDetails() {
return <Popover.Root>
<Popover.Trigger render={<Button />}>View details</Popover.Trigger>
<Popover.Portal><Popover.Positioner><Popover.Popup>
<Popover.Title>A little more information</Popover.Title>
<Popover.Description>This route takes about an hour.</Popover.Description>
<Popover.Close aria-label="Close place details" />
</Popover.Popup></Popover.Positioner></Popover.Portal>
</Popover.Root>;
}API reference: Popover, Button.
Usage contract
Tooltip describes an existing control; it should not contain essential interactive actions. Preview cards are enhancements to links. Popper is collision-aware positioning only and deliberately supplies no modal role or focus policy.
Portaled positioners use positionMethod="fixed" by default so revealing popup content cannot scroll a locked document body. Position, anchor tracking and collision handling stay relative to the viewport. Pass positionMethod="absolute" explicitly when a custom containing block requires it.
Customization
Use semantic tokens for visual changes, public component parts for structural customization, and explicit props for instance behavior. The surrounding site lets you compare light and dark, different brands and density.
Source and verification
The examples below import actual public package entries. Full source is downloadable. Source coverage does not establish keyboard, hydration or browser compatibility; those require the application test suite.
Arrow artwork and scrolling
Popover.Arrow, Tooltip.Arrow, PreviewCard.Arrow, Menu.Arrow, and Autocomplete.Arrow provide a small bordered pointer by default. Place the arrow inside its popup. The positioning engine aligns it with the trigger and handles collision-driven side changes, including logical sides in RTL. Tooltip and preview-card tone variants also color the pointer. Pass children or render to supply custom artwork.
Popups containing an arrow fade on entry and exit; keeping their transform unset allows the pointer to extend beyond the scrollable popup. Popups without arrows retain their scale transition. Both respect reduced motion. Keep the default static positioning on a scrollable popup with an arrow; custom transforms or positioning can make it clip its pointer.
Cursor tracking and touch behavior
Tooltip.Root accepts trackCursorAxis="x", "y" or "both" to follow a pointer; Positioner also accepts a virtual anchor. Standard hover/focus, delays, controlled open state and interactive content remain available. TouchTooltip adds touch-hold activation with configurable delay and movement cancellation. Use it when a touch description is useful; a normal tap still activates the wrapped control. Essential information should remain available without a hover or hold gesture.
Scrollable popovers, preview cards, tooltips and menus contain overscroll at their boundaries. Reaching the end of their content keeps the page and anchor in place; scrolling outside the popup remains available for non-modal compositions.
Floating surfaces also guard vertical wheel handoff at an actual content boundary in hosts that ignore CSS containment. Nested scrolling content keeps its own wheel behavior; native input/select controls, horizontal/shift-wheel input and browser zoom gestures are left to their normal behavior. This guard is scoped to the rendered popup and is removed with its ref.
Live examples
Try the controls below, then read or copy the complete example to adapt it for your application.
Cursor-following tooltip
Positioning without modal behavior
Popper only positions content. Applications provide semantics and dismissal where appropriate.
Explicit portal and backdrop primitives
This decorative backdrop preview does not create a modal or lock focus.
Read the complete example
'use client';
import { useRef, useState } from 'react';
import { Popover } from '@fayegram-ai/faye-ui/overlays/popover';
import { Tooltip } from '@fayegram-ai/faye-ui/overlays/tooltip';
import { PreviewCard } from '@fayegram-ai/faye-ui/overlays/preview-card';
import { Portal } from '@fayegram-ai/faye-ui/overlays/portal';
import { Backdrop } from '@fayegram-ai/faye-ui/overlays/backdrop';
import { Popper } from '@fayegram-ai/faye-ui/overlays/popper';
import { ClickAwayListener } from '@fayegram-ai/faye-ui/hooks/click-away';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { PortalTheme } from '@fayegram-ai/faye-ui/provider/provider';
import { Section, Result } from '../example';
import { Choice } from '../choice';
export default function Floating() {
const [open, setOpen] = useState(false), [anchor, setAnchor] = useState<HTMLElement | null>(null), [portal, setPortal] = useState(false), [away, setAway] = useState(0);
const [side, setSide] = useState<'top' | 'bottom' | 'left' | 'right'>('bottom');
const portalTarget = useRef<HTMLDivElement>(null);
return <><Section title="Cursor-following tooltip"><Tooltip.Root trackCursorAxis="both"><Tooltip.Trigger delay={0} render={<Button/>}>Follow the pointer</Tooltip.Trigger><Tooltip.Portal><Tooltip.Positioner><Tooltip.Popup>Position follows the pointer while the description stays associated.</Tooltip.Popup></Tooltip.Positioner></Tooltip.Portal></Tooltip.Root></Section><Section title="Popover, tooltip and preview card"><Inline wrap><Choice label="Floating panel side" value={side} onValueChange={value => setSide(value as typeof side)} options={['top','bottom','left','right'].map(value=>({value,label:value}))}/></Inline><Inline wrap><Popover.Root><Popover.Trigger render={<Button />}>View details</Popover.Trigger><Popover.Portal><Popover.Positioner side={side}><Popover.Popup><Popover.Arrow/><Stack gap="3"><Popover.Title>Workspace visibility</Popover.Title><Popover.Description>People with access can view these resources.</Popover.Description><Popover.Close render={<Button />}>Understood</Popover.Close></Stack></Popover.Popup></Popover.Positioner></Popover.Portal></Popover.Root><Tooltip.Provider><Tooltip.Root><Tooltip.Trigger render={<Button aria-label="Show keyboard shortcut"/>}>Shortcut</Tooltip.Trigger><Tooltip.Portal><Tooltip.Positioner side={side}><Tooltip.Popup><Tooltip.Arrow/>Use Ctrl or Command K to search docs.</Tooltip.Popup></Tooltip.Positioner></Tooltip.Portal></Tooltip.Root></Tooltip.Provider><PreviewCard.Root><PreviewCard.Trigger href="/docs/theming/">Preview theming</PreviewCard.Trigger><PreviewCard.Portal><PreviewCard.Positioner side={side}><PreviewCard.Popup><PreviewCard.Arrow/><strong>Theme system</strong><p>Typed tokens, schemes, density and scoped component profiles.</p></PreviewCard.Popup></PreviewCard.Positioner></PreviewCard.Portal></PreviewCard.Root></Inline></Section><Section title="Positioning without modal behavior" note="Popper only positions content. Applications provide semantics and dismissal where appropriate."><Button ref={setAnchor} aria-expanded={open} aria-controls="position-example" onClick={() => setOpen(value => !value)}>Toggle anchored note</Button><Popper open={open} anchor={anchor}><div id="position-example" role="status" className="docs-layout-cell">Anchored to the button, collision-aware.</div></Popper><ClickAwayListener onClickAway={() => setAway(n => n + 1)}><div className="docs-layout-cell">Click inside, then outside this region.</div></ClickAwayListener><Result>Outside pointer events: {away}</Result></Section><Section title="Explicit portal and backdrop primitives" note="This decorative backdrop preview does not create a modal or lock focus."><Button onClick={() => setPortal(p => !p)}>Toggle portal content</Button><div ref={portalTarget} style={{ minHeight: '6rem', position: 'relative', isolation: 'isolate' }} className="docs-layout-cell"><Backdrop style={{ position: 'absolute', zIndex: -1 }}/><span>Portal destination</span></div>{portal && portalTarget.current && <Portal container={portalTarget.current}><PortalTheme><div role="status">Rendered into an explicit container.</div></PortalTheme></Portal>}</Section></>;
}