- Replace .eslintrc.cjs with eslint.config.mjs (ESLint 9 flat config)
using direct eslint-plugin-solid + @typescript-eslint/parser approach
- Add @typescript-eslint/parser to root devDependencies
- Add main/module/types top-level fields to packages/core/package.json
- Add resolve.conditions to packages/core/vite.config.ts
- Create packages/core/tsconfig.test.json for test type-checking
- Remove empty paths:{} from packages/core/tsconfig.json
705 lines
22 KiB
Markdown
705 lines
22 KiB
Markdown
[Skip to main content](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026#main-content)
|
|
|
|
# [Floating UI vs Tippy.js vs Radix Tooltip: Popover Positioning 2026](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#floating-ui-vs-tippyjs-vs-radix-tooltip-popover-positioning-2026)
|
|
|
|
## [TL;DR](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tldr)
|
|
|
|
Building tooltips, popovers, dropdowns, and floating menus correctly is deceptively hard — viewport overflow, collision detection, scroll containers, and keyboard accessibility are all gotchas that custom solutions routinely miss. **Floating UI** (successor to Popper.js from the same authors) is the low-level positioning engine — pure geometry and collision detection, totally unstyled, works with any framework, and is what Radix, Mantine, and many others use internally. **Tippy.js** is the batteries-included tooltip library built on Popper.js — styled out of the box, declarative API, animates, works in vanilla JS and React — but it's showing its age in 2026 with no App Router support and weaker accessibility guarantees. **Radix UI's Tooltip and Popover** are headless, fully accessible (WAI-ARIA compliant), React-only components built on Floating UI internally — the correct choice for React/Next.js component libraries where accessibility is non-negotiable. For low-level control over positioning in any framework: Floating UI. For quick tooltips with minimal config: Tippy.js. For production React UIs that must be accessible: Radix Tooltip/Popover.
|
|
|
|
## [Key Takeaways](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#key-takeaways)
|
|
|
|
- **Floating UI is framework-agnostic** — core is vanilla JS, `@floating-ui/react` adds React hooks
|
|
- **Floating UI handles all edge cases** — viewport overflow, flip, shift, arrow, virtual elements
|
|
- **Tippy.js is easiest to get started** — `<Tippy content="Tooltip">` wraps any element
|
|
- **Radix Tooltip is fully WAI-ARIA compliant** — focus management, screen readers, keyboard nav
|
|
- **Tippy.js is built on Popper.js** — Floating UI's predecessor, still maintained but less active
|
|
- **Radix Popover manages open state** — controlled and uncontrolled modes, portal rendering
|
|
- **Floating UI powers Radix internally** — Radix uses `@floating-ui/react-dom` under the hood
|
|
|
|
* * *
|
|
|
|
## [Use Case Map](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#use-case-map)
|
|
|
|
```
|
|
Simple tooltip on hover → Tippy.js or Radix Tooltip
|
|
Tooltip with custom render → Floating UI or Radix Tooltip
|
|
Accessible popover with content → Radix Popover
|
|
Dropdown menu with keyboard nav → Radix DropdownMenu
|
|
Custom positioning engine → Floating UI (raw)
|
|
Framework-agnostic tooltip → Tippy.js or Floating UI
|
|
Select/Combobox overlay → Floating UI or Radix Select
|
|
Context menu (right-click) → Radix ContextMenu
|
|
```
|
|
|
|
* * *
|
|
|
|
## [Floating UI: The Positioning Engine](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#floating-ui-the-positioning-engine)
|
|
|
|
Floating UI provides the geometry and collision detection algorithms — you wire up the DOM refs and React state yourself.
|
|
|
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation)
|
|
|
|
```bash
|
|
npm install @floating-ui/react
|
|
# For vanilla JS (no React):
|
|
npm install @floating-ui/dom
|
|
```
|
|
|
|
### [Basic Tooltip](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#basic-tooltip)
|
|
|
|
```tsx
|
|
import {
|
|
useFloating,
|
|
autoUpdate,
|
|
offset,
|
|
flip,
|
|
shift,
|
|
useHover,
|
|
useFocus,
|
|
useDismiss,
|
|
useRole,
|
|
useInteractions,
|
|
FloatingPortal,
|
|
} from "@floating-ui/react";
|
|
import { useState } from "react";
|
|
|
|
interface TooltipProps {
|
|
content: string;
|
|
children: React.ReactElement;
|
|
}
|
|
|
|
export function Tooltip({ content, children }: TooltipProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const { refs, floatingStyles, context } = useFloating({
|
|
open: isOpen,
|
|
onOpenChange: setIsOpen,
|
|
placement: "top",
|
|
// Keep in sync with scroll and resize
|
|
whileElementsMounted: autoUpdate,
|
|
middleware: [\
|
|
offset(8), // Distance from reference\
|
|
flip(), // Flip to bottom if no space above\
|
|
shift({ padding: 8 }), // Shift horizontally to stay in viewport\
|
|
],
|
|
});
|
|
|
|
// Interaction hooks — compose behaviors
|
|
const hover = useHover(context, { move: false });
|
|
const focus = useFocus(context);
|
|
const dismiss = useDismiss(context);
|
|
const role = useRole(context, { role: "tooltip" });
|
|
|
|
const { getReferenceProps, getFloatingProps } = useInteractions([\
|
|
hover,\
|
|
focus,\
|
|
dismiss,\
|
|
role,\
|
|
]);
|
|
|
|
return (
|
|
<>
|
|
{/* Attach to trigger element */}
|
|
{React.cloneElement(children, {
|
|
ref: refs.setReference,
|
|
...getReferenceProps(),
|
|
})}
|
|
|
|
{/* Tooltip — rendered in portal to escape stacking contexts */}
|
|
<FloatingPortal>
|
|
{isOpen && (
|
|
<div
|
|
ref={refs.setFloating}
|
|
style={{
|
|
...floatingStyles,
|
|
background: "#1a1a1a",
|
|
color: "#fff",
|
|
padding: "4px 8px",
|
|
borderRadius: 4,
|
|
fontSize: 12,
|
|
zIndex: 9999,
|
|
}}
|
|
{...getFloatingProps()}
|
|
>
|
|
{content}
|
|
</div>
|
|
)}
|
|
</FloatingPortal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<Tooltip content="Copy to clipboard">
|
|
<button>Copy</button>
|
|
</Tooltip>
|
|
```
|
|
|
|
### [Arrow Placement](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#arrow-placement)
|
|
|
|
```tsx
|
|
import {
|
|
useFloating,
|
|
arrow,
|
|
offset,
|
|
flip,
|
|
FloatingArrow,
|
|
} from "@floating-ui/react";
|
|
import { useRef } from "react";
|
|
|
|
export function TooltipWithArrow({ content, children }: TooltipProps) {
|
|
const arrowRef = useRef<SVGSVGElement>(null);
|
|
|
|
const { refs, floatingStyles, context, middlewareData, placement } = useFloating({
|
|
middleware: [\
|
|
offset(10),\
|
|
flip(),\
|
|
arrow({ element: arrowRef }),\
|
|
],
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<div ref={refs.setReference}>{children}</div>
|
|
|
|
<div ref={refs.setFloating} style={floatingStyles}>
|
|
{content}
|
|
{/* FloatingArrow renders an SVG arrow positioned correctly */}
|
|
<FloatingArrow
|
|
ref={arrowRef}
|
|
context={context}
|
|
fill="#1a1a1a"
|
|
height={8}
|
|
width={14}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Popover (Click-to-Open)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#popover-click-to-open)
|
|
|
|
```tsx
|
|
import {
|
|
useFloating,
|
|
autoUpdate,
|
|
offset,
|
|
flip,
|
|
shift,
|
|
useClick,
|
|
useDismiss,
|
|
useRole,
|
|
useInteractions,
|
|
FloatingPortal,
|
|
FloatingFocusManager,
|
|
} from "@floating-ui/react";
|
|
import { useState } from "react";
|
|
|
|
export function Popover({ trigger, content }: { trigger: React.ReactNode; content: React.ReactNode }) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const { refs, floatingStyles, context } = useFloating({
|
|
open: isOpen,
|
|
onOpenChange: setIsOpen,
|
|
placement: "bottom-start",
|
|
whileElementsMounted: autoUpdate,
|
|
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
});
|
|
|
|
const click = useClick(context);
|
|
const dismiss = useDismiss(context);
|
|
const role = useRole(context, { role: "dialog" });
|
|
|
|
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
|
|
|
return (
|
|
<>
|
|
<div ref={refs.setReference} {...getReferenceProps()}>
|
|
{trigger}
|
|
</div>
|
|
|
|
<FloatingPortal>
|
|
{isOpen && (
|
|
// FloatingFocusManager traps focus inside the popover
|
|
<FloatingFocusManager context={context} modal={false}>
|
|
<div
|
|
ref={refs.setFloating}
|
|
style={{
|
|
...floatingStyles,
|
|
background: "#fff",
|
|
border: "1px solid #e2e8f0",
|
|
borderRadius: 8,
|
|
boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
|
|
padding: 16,
|
|
zIndex: 9999,
|
|
minWidth: 200,
|
|
}}
|
|
{...getFloatingProps()}
|
|
>
|
|
{content}
|
|
</div>
|
|
</FloatingFocusManager>
|
|
)}
|
|
</FloatingPortal>
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Virtual Element (Context Menu)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#virtual-element-context-menu)
|
|
|
|
```tsx
|
|
import { useFloating, offset, flip, shift, useClientPoint, useInteractions } from "@floating-ui/react";
|
|
import { useState } from "react";
|
|
|
|
export function ContextMenu({ items }: { items: string[] }) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const { refs, floatingStyles, context } = useFloating({
|
|
open: isOpen,
|
|
onOpenChange: setIsOpen,
|
|
placement: "bottom-start",
|
|
middleware: [offset({ mainAxis: 5, alignmentAxis: 4 }), flip(), shift()],
|
|
});
|
|
|
|
// Follow the mouse cursor
|
|
const clientPoint = useClientPoint(context);
|
|
const { getReferenceProps, getFloatingProps } = useInteractions([clientPoint]);
|
|
|
|
return (
|
|
<div
|
|
ref={refs.setReference}
|
|
onContextMenu={(e) => {
|
|
e.preventDefault();
|
|
setIsOpen(true);
|
|
}}
|
|
style={{ minHeight: 200, border: "1px dashed #ccc", padding: 16 }}
|
|
{...getReferenceProps()}
|
|
>
|
|
Right-click anywhere here
|
|
|
|
{isOpen && (
|
|
<div
|
|
ref={refs.setFloating}
|
|
style={{
|
|
...floatingStyles,
|
|
background: "#fff",
|
|
border: "1px solid #e2e8f0",
|
|
borderRadius: 6,
|
|
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
|
zIndex: 9999,
|
|
}}
|
|
{...getFloatingProps()}
|
|
>
|
|
{items.map((item) => (
|
|
<button
|
|
key={item}
|
|
style={{ display: "block", width: "100%", padding: "8px 16px", textAlign: "left" }}
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
* * *
|
|
|
|
## [Tippy.js: Batteries-Included Tooltips](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tippyjs-batteries-included-tooltips)
|
|
|
|
Tippy.js provides a complete tooltip and popover solution with themes, animations, and a declarative API — minimal configuration required.
|
|
|
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation-1)
|
|
|
|
```bash
|
|
npm install tippy.js @tippyjs/react
|
|
```
|
|
|
|
### [Basic Usage](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#basic-usage)
|
|
|
|
```tsx
|
|
import Tippy from "@tippyjs/react";
|
|
import "tippy.js/dist/tippy.css"; // Default theme
|
|
|
|
export function CopyButton() {
|
|
return (
|
|
<Tippy content="Copy to clipboard">
|
|
<button onClick={() => navigator.clipboard.writeText("text")}>
|
|
Copy
|
|
</button>
|
|
</Tippy>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Placement and Options](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#placement-and-options)
|
|
|
|
```tsx
|
|
import Tippy from "@tippyjs/react";
|
|
|
|
export function FeatureTooltips() {
|
|
return (
|
|
<div>
|
|
<Tippy content="Shows above" placement="top">
|
|
<button>Top</button>
|
|
</Tippy>
|
|
|
|
<Tippy content="Shows on the right" placement="right">
|
|
<button>Right</button>
|
|
</Tippy>
|
|
|
|
{/* Delay: 300ms show, 100ms hide */}
|
|
<Tippy content="Delayed tooltip" delay={[300, 100]}>
|
|
<button>Delayed</button>
|
|
</Tippy>
|
|
|
|
{/* Click to toggle instead of hover */}
|
|
<Tippy content="Click me" trigger="click" interactive>
|
|
<button>Click</button>
|
|
</Tippy>
|
|
|
|
{/* Interactive (won't close when hovering tooltip) */}
|
|
<Tippy
|
|
content={
|
|
<div>
|
|
<strong>Rich content</strong>
|
|
<p>With multiple elements</p>
|
|
<a href="/docs">Read more</a>
|
|
</div>
|
|
}
|
|
interactive
|
|
interactiveBorder={20}
|
|
placement="bottom"
|
|
>
|
|
<button>Hover for rich tooltip</button>
|
|
</Tippy>
|
|
|
|
{/* Disabled */}
|
|
<Tippy content="Tooltip" disabled={false}>
|
|
<span>
|
|
<button disabled>Disabled Button</button>
|
|
</span>
|
|
</Tippy>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Animations and Themes](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#animations-and-themes)
|
|
|
|
```tsx
|
|
import Tippy from "@tippyjs/react";
|
|
import "tippy.js/dist/tippy.css";
|
|
import "tippy.js/animations/scale.css";
|
|
import "tippy.js/themes/light.css";
|
|
import "tippy.js/themes/material.css";
|
|
|
|
export function ThemedTooltips() {
|
|
return (
|
|
<>
|
|
{/* Built-in light theme */}
|
|
<Tippy content="Light theme" theme="light">
|
|
<button>Light</button>
|
|
</Tippy>
|
|
|
|
{/* Scale animation */}
|
|
<Tippy content="Animated" animation="scale">
|
|
<button>Scale</button>
|
|
</Tippy>
|
|
|
|
{/* Custom theme via CSS */}
|
|
<Tippy content="Custom theme" className="custom-tippy">
|
|
<button>Custom</button>
|
|
</Tippy>
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Controlled Tippy](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#controlled-tippy)
|
|
|
|
```tsx
|
|
import Tippy from "@tippyjs/react";
|
|
import { useState } from "react";
|
|
|
|
export function ControlledTooltip() {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
return (
|
|
<Tippy
|
|
content="This is controlled"
|
|
visible={visible}
|
|
onClickOutside={() => setVisible(false)}
|
|
interactive
|
|
>
|
|
<button onClick={() => setVisible((v) => !v)}>
|
|
{visible ? "Hide" : "Show"} Tooltip
|
|
</button>
|
|
</Tippy>
|
|
);
|
|
}
|
|
```
|
|
|
|
* * *
|
|
|
|
## [Radix UI Tooltip and Popover: Accessible Components](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#radix-ui-tooltip-and-popover-accessible-components)
|
|
|
|
Radix provides fully accessible, headless components with correct ARIA roles, focus management, and keyboard navigation — you bring your own styles.
|
|
|
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation-2)
|
|
|
|
```bash
|
|
npm install @radix-ui/react-tooltip @radix-ui/react-popover
|
|
```
|
|
|
|
### [Tooltip](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tooltip)
|
|
|
|
```tsx
|
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
|
|
// Provider wraps your app — controls delay behavior globally
|
|
export function App() {
|
|
return (
|
|
<Tooltip.Provider delayDuration={300} skipDelayDuration={500}>
|
|
<YourApp />
|
|
</Tooltip.Provider>
|
|
);
|
|
}
|
|
|
|
// Individual tooltip
|
|
export function DeleteButton() {
|
|
return (
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger asChild>
|
|
<button className="icon-button" aria-label="Delete item">
|
|
🗑️
|
|
</button>
|
|
</Tooltip.Trigger>
|
|
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content
|
|
className="tooltip-content"
|
|
sideOffset={4}
|
|
side="top"
|
|
align="center"
|
|
>
|
|
Delete item
|
|
<Tooltip.Arrow className="tooltip-arrow" />
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
);
|
|
}
|
|
|
|
// CSS
|
|
/*
|
|
.tooltip-content {
|
|
background: #1a1a1a;
|
|
color: white;
|
|
border-radius: 4px;
|
|
padding: 4px 10px;
|
|
font-size: 13px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
animation-duration: 150ms;
|
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
will-change: transform, opacity;
|
|
}
|
|
.tooltip-content[data-state='delayed-open'][data-side='top'] {
|
|
animation-name: slideDownAndFade;
|
|
}
|
|
@keyframes slideDownAndFade {
|
|
from { opacity: 0; transform: translateY(2px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.tooltip-arrow {
|
|
fill: #1a1a1a;
|
|
}
|
|
*/
|
|
```
|
|
|
|
### [Popover](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#popover)
|
|
|
|
```tsx
|
|
import * as Popover from "@radix-ui/react-popover";
|
|
|
|
export function FilterPopover() {
|
|
return (
|
|
<Popover.Root>
|
|
<Popover.Trigger asChild>
|
|
<button className="filter-button">Filters ⚙️</button>
|
|
</Popover.Trigger>
|
|
|
|
<Popover.Portal>
|
|
<Popover.Content
|
|
className="popover-content"
|
|
sideOffset={4}
|
|
align="start"
|
|
// Prevent closing when focus moves inside popover
|
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
>
|
|
<div className="filter-form">
|
|
<h3>Filter Options</h3>
|
|
|
|
<label>
|
|
Status
|
|
<select>
|
|
<option>All</option>
|
|
<option>Active</option>
|
|
<option>Inactive</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
Date Range
|
|
<input type="date" />
|
|
</label>
|
|
|
|
<div className="filter-actions">
|
|
<button>Reset</button>
|
|
<Popover.Close asChild>
|
|
<button>Apply</button>
|
|
</Popover.Close>
|
|
</div>
|
|
</div>
|
|
|
|
<Popover.Arrow className="popover-arrow" />
|
|
<Popover.Close className="popover-close" aria-label="Close">
|
|
✕
|
|
</Popover.Close>
|
|
</Popover.Content>
|
|
</Popover.Portal>
|
|
</Popover.Root>
|
|
);
|
|
}
|
|
```
|
|
|
|
### [Tooltip with Tailwind (shadcn/ui Pattern)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tooltip-with-tailwind-shadcnui-pattern)
|
|
|
|
```tsx
|
|
// components/ui/tooltip.tsx — shadcn/ui Tooltip component
|
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const TooltipProvider = TooltipPrimitive.Provider;
|
|
const TooltipRoot = TooltipPrimitive.Root;
|
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
|
|
const TooltipContent = React.forwardRef<
|
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
<TooltipPrimitive.Portal>
|
|
<TooltipPrimitive.Content
|
|
ref={ref}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95",
|
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</TooltipPrimitive.Portal>
|
|
));
|
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
|
|
// Export the Tooltip component
|
|
export function Tooltip({
|
|
children,
|
|
content,
|
|
...props
|
|
}: {
|
|
children: React.ReactNode;
|
|
content: React.ReactNode;
|
|
} & React.ComponentPropsWithoutRef<typeof TooltipRoot>) {
|
|
return (
|
|
<TooltipProvider>
|
|
<TooltipRoot {...props}>
|
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
<TooltipContent>{content}</TooltipContent>
|
|
</TooltipRoot>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
// Usage with Tailwind
|
|
<Tooltip content="Settings">
|
|
<button>⚙️</button>
|
|
</Tooltip>
|
|
```
|
|
|
|
* * *
|
|
|
|
## [Feature Comparison](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#feature-comparison)
|
|
|
|
| Feature | Floating UI | Tippy.js | Radix Tooltip/Popover |
|
|
| --- | --- | --- | --- |
|
|
| **Framework** | Any (React, Vue, Svelte) | Any + React | React only |
|
|
| **Styling** | Unstyled (bring your own) | Styled (override available) | Unstyled (bring your own) |
|
|
| **Accessibility** | Manual (you implement) | Basic | ✅ WAI-ARIA compliant |
|
|
| **Focus trap** | `FloatingFocusManager` | No | ✅ Built-in |
|
|
| **Keyboard nav** | Via hooks | Basic | ✅ Built-in |
|
|
| **Collision detection** | ✅ Advanced | ✅ Via Popper.js | ✅ Via Floating UI |
|
|
| **Arrow positioning** | ✅ `FloatingArrow` | ✅ Built-in | ✅ `Tooltip.Arrow` |
|
|
| **Animations** | CSS (you define) | ✅ Built-in themes | CSS data-state |
|
|
| **Portal** | ✅ `FloatingPortal` | ✅ Auto | ✅ `Portal` |
|
|
| **Virtual elements** | ✅ | Limited | No |
|
|
| **Bundle size** | ~10kB | ~15kB | ~8kB per primitive |
|
|
| **npm weekly** | 12M | 3M | 8M (tooltip) |
|
|
| **GitHub stars** | 29k | 11k | 22k (radix-ui/primitives) |
|
|
| **TypeScript** | ✅ Full | ✅ | ✅ Full |
|
|
|
|
* * *
|
|
|
|
## [When to Use Each](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#when-to-use-each)
|
|
|
|
**Choose Floating UI if:**
|
|
|
|
- Building a component library from scratch (unstyled primitives)
|
|
- Need maximum control over positioning behavior and styling
|
|
- Framework-agnostic — Vue, Svelte, vanilla JS, or React
|
|
- Virtual element positioning (context menus, cursors)
|
|
- Complex middleware requirements (custom offset logic)
|
|
- Want to understand exactly what's happening — no magic
|
|
|
|
**Choose Tippy.js if:**
|
|
|
|
- Quick tooltip needed with minimal setup
|
|
- Vanilla JS project or legacy codebase
|
|
- Want built-in themes and animations without CSS work
|
|
- Simple hover tooltips where accessibility is secondary
|
|
- Prototyping or internal tools where ARIA isn't critical
|
|
|
|
**Choose Radix Tooltip/Popover if:**
|
|
|
|
- React/Next.js production application
|
|
- Accessibility is required — screen readers, keyboard navigation
|
|
- Using shadcn/ui (Radix is the foundation)
|
|
- Want compound component API with proper focus management
|
|
- Need `asChild` pattern to avoid extra DOM elements
|
|
- Building a design system where consumers control all styling
|
|
|
|
* * *
|
|
|
|
## [Methodology](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#methodology)
|
|
|
|
Data sourced from Floating UI documentation (floating-ui.com/docs), Tippy.js documentation (atomiks.github.io/tippyjs), Radix UI documentation (radix-ui.com/docs), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and community discussions from the React Discord, CSS-Tricks, and web accessibility forums.
|
|
|
|
* * *
|
|
|
|
_Related: [Radix UI vs Headless UI vs Ariakit](https://www.pkgpulse.com/blog/radix-ui-vs-headless-ui-vs-ariakit-accessible-react-components-2026) for broader headless component comparisons, or [shadcn/ui vs Mantine vs Chakra UI](https://www.pkgpulse.com/blog/shadcn-vs-mantine-vs-chakra-react-component-library-2026) for styled React component libraries._
|
|
|
|
## Comments
|
|
|
|
### The 2026 JavaScript Stack Cheatsheet
|
|
|
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
|
|
|
Get the Free Cheatsheet |