react-popper-tooltip vs react-tippy vs react-tooltip
Architectural Choices for React Tooltip Implementations
react-popper-tooltipreact-tippyreact-tooltipSimilar Packages:

Architectural Choices for React Tooltip Implementations

react-popper-tooltip, react-tippy, and react-tooltip are all libraries designed to render contextual information (tooltips) relative to a target element in React applications. While they share the same goal, their underlying architectures differ significantly. react-popper-tooltip acts as a low-level hook-based wrapper around Popper.js, offering maximum flexibility for custom UI construction. react-tippy was a popular component-based wrapper for Tippy.js that is now deprecated. react-tooltip (formerly react-tooltip-lite) provides a modern, declarative component API with built-in styling and theming capabilities, aiming for a balance between ease of use and customization.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-popper-tooltip024993.3 kB13-MIT
react-tippy0977-1036 years agoMIT
react-tooltip03,8211.06 MB03 months agoMIT

React Tooltip Libraries: Architecture, Maintenance, and DX Compared

When adding tooltips to a React application, the choice of library dictates not just how the tooltip looks, but how it interacts with your component tree, how you handle state, and whether your project remains maintainable in the future. react-popper-tooltip, react-tippy, and react-tooltip represent three distinct approaches: a low-level hook system, a deprecated component wrapper, and a modern declarative component.

⚠️ Critical Maintenance Status: The Elephant in the Room

Before discussing features, we must address the lifecycle status of these packages, as this is the most critical architectural decision factor.

react-tippy is deprecated. The maintainers have officially marked this package as unmaintained. It relies on older versions of Tippy.js and does not support modern React features like concurrent rendering or strict mode safely. Using it in new projects introduces technical debt immediately.

// react-tippy: DO NOT USE in new projects
// This package is deprecated and may break in React 18+
import { Tooltip } from 'react-tippy';

// Legacy usage pattern (Avoid)
<Tooltip title="Delete user" position="top">
  <button>Delete</button>
</Tooltip>

In contrast, both react-popper-tooltip and react-tooltip are actively maintained. react-tooltip (v5+) underwent a complete rewrite to fix long-standing issues, while react-popper-tooltip continues to serve as a stable, low-level utility.

🏗️ Architecture: Hooks vs. Components

The fundamental difference lies in how much control the library gives you versus how much it manages for you.

react-popper-tooltip exposes raw power through React hooks. It does not render any HTML for you. Instead, it gives you the positioning logic and state management, leaving the rendering entirely up to your component. This is perfect for custom design systems but requires more code.

// react-popper-tooltip: You build the UI
import { useTooltip, useTooltipState, TooltipContainer, TooltipTrigger } from 'react-popper-tooltip';

function CustomTooltip() {
  const { getTriggerProps, state } = useTooltip();
  const { getTooltipProps, arrowRef, tooltipRef } = useTooltipState(state);

  return (
    <>
      <button {...getTriggerProps()}>Hover me</button>
      {state.show && (
        <div {...getTooltipProps({ ref: tooltipRef })} className="my-custom-class">
          <div ref={arrowRef} className="my-arrow" />
          <p>Custom content here</p>
        </div>
      )}
    </>
  );
}

react-tooltip abstracts this complexity away. You declare a target element and a separate tooltip component, and the library handles the mounting, positioning, and visibility logic internally. This results in cleaner JSX but less flexibility in structural markup.

// react-tooltip: Declarative approach
import { Tooltip } from 'react-tooltip';

function Dashboard() {
  return (
    <>
      <button data-tooltip-id="my-tooltip">Hover me</button>
      <Tooltip id="my-tooltip" content="Delete user" place="top" />
    </>
  );
}

react-tippy (historically) used a wrapper component approach similar to react-tooltip but with less flexibility in separating concerns. Since it is deprecated, this pattern is no longer recommended unless migrated to the official @tippyjs/react.

🎨 Styling and Theming Strategies

How you style the tooltip varies wildly between these options, impacting your CSS architecture.

react-popper-tooltip requires you to bring your own styles. It provides no default CSS. This means you have full control over animations, shadows, and colors, but you must write them from scratch or integrate a utility library like Tailwind CSS directly into your rendered elements.

// react-popper-tooltip: Full CSS control
<div 
  {...getTooltipProps({ ref: tooltipRef })} 
  className="bg-black text-white p-2 rounded shadow-lg z-50"
>
  Content
</div>

react-tooltip ships with built-in themes and a robust styling API. You can pass standard CSS classes or use the className prop to override defaults. It also supports global theming via context, allowing you to set default colors and fonts for all tooltips in your app.

// react-tooltip: Built-in theming
<Tooltip 
  id="global-tooltip" 
  className="my-tooltip-class" 
  style={{ backgroundColor: '#333' }}
  variant="success" // Built-in variants
/>

react-tippy relied on Tippy.js themes which were often loaded via global CSS imports. This approach can lead to style conflicts in modern modular CSS environments and is difficult to scope to specific components.

🔄 Handling Dynamic Content and Portals

Tooltips often need to render outside their parent container (using Portals) to avoid z-index clipping or overflow issues.

react-popper-tooltip handles portals automatically if you use the provided components, but since you are rendering the DOM nodes, you can easily wrap the tooltip content in a React Portal manually if your layout demands it. This offers the highest degree of control for complex layouts like modals or fixed headers.

// react-popper-tooltip: Manual Portal integration
import { createPortal } from 'react-dom';

{state.show && createPortal(
  <div {...getTooltipProps({ ref: tooltipRef })}>
    Content
  </div>,
  document.body
)}

react-tooltip manages portals internally. By default, it renders the tooltip at the end of the body to prevent z-index issues. You generally don't need to think about this, which is a benefit for speed but a limitation if you need the tooltip to remain inside a specific shadow DOM or container context.

// react-tooltip: Automatic portal handling
// No extra code needed; library handles mounting to body
<Tooltip id="my-tip" content="Auto-ported to body" />

react-tippy also used portals but had known issues with React Strict Mode and concurrent rendering, often causing tooltips to flicker or detach unexpectedly in newer React versions.

♿ Accessibility and Interaction Patterns

Accessibility is non-negotiable for production apps. All three libraries aim to support ARIA standards, but their implementation quality differs.

react-popper-tooltip gives you the tools to build accessible tooltips but requires you to implement the ARIA attributes correctly. It provides props like aria-labelledby and manages focus trapping if configured, but the responsibility lies with the developer to ensure the trigger and content are linked semantically.

// react-popper-tooltip: Manual ARIA management
<button 
  {...getTriggerProps()} 
  aria-describedby={state.show ? tooltipId : undefined}
>
  Trigger
</button>

react-tooltip automates much of this. It automatically adds role="tooltip" and manages the aria-describedby relationship between the target and the tooltip. It also supports keyboard interactions (Escape to close) out of the box.

// react-tooltip: Automated accessibility
// Library automatically links aria-describedby
<button data-tooltip-id="tip">Hover</button>
<Tooltip id="tip" content="Info" />

react-tippy had decent accessibility support in its prime but lacks updates for newer WCAG guidelines and React accessibility patterns.

📊 Summary: Key Differences

Featurereact-popper-tooltipreact-tippyreact-tooltip
Status✅ Active❌ Deprecated✅ Active
API StyleHooks & Render PropsComponent WrapperDeclarative Component
StylingDIY (No defaults)Global ThemesBuilt-in Themes & Props
ControlMaximum (You build UI)Low (Pre-built)Medium (Configurable)
Bundle ImpactLow (Tree-shakable)High (Legacy deps)Medium (Feature-rich)
Best ForDesign SystemsNone (Migrate)Rapid Development

💡 The Big Picture

react-popper-tooltip is the engineer's choice. It is like buying raw lumber and nails to build a house — you get exactly the structure you want, but you have to do the work. Use this when your design requirements are unique and cannot be met by standard tooltip components.

react-tippy is a legacy artifact. It is like trying to repair a car model that hasn't been manufactured for ten years. While it might run, parts are scarce, and it won't pass modern safety inspections. Migrate away from it immediately.

react-tooltip is the product manager's choice. It is like buying a pre-fabricated home — it arrives quickly, looks good, meets code standards, and covers 90% of use cases perfectly. Use this for standard admin dashboards, SaaS products, and marketing sites where development speed and consistency are priorities.

Final Thought: In modern React architecture, avoid react-tippy. If you need speed and consistency, pick react-tooltip. If you need pixel-perfect customization for a design system, pick react-popper-tooltip.

How to Choose: react-popper-tooltip vs react-tippy vs react-tooltip

  • react-popper-tooltip:

    Choose react-popper-tooltip if you need complete control over the tooltip's HTML structure and styling without being constrained by a pre-built component. It is ideal for design systems where the tooltip must match a specific, unique visual identity that differs from standard library defaults. Be prepared to write more boilerplate code to handle state, positioning logic, and accessibility attributes manually using the provided hooks.

  • react-tippy:

    Do NOT choose react-tippy for any new project. This package is deprecated and no longer maintained, as it relied on older versions of Tippy.js and React patterns that are now obsolete. Using it introduces security risks and compatibility issues with modern React versions. You should migrate existing implementations to @tippyjs/react (the official headless wrapper) or switch to react-tooltip for a managed solution.

  • react-tooltip:

    Choose react-tooltip if you want a robust, drop-in solution that handles positioning, theming, and accessibility out of the box with minimal configuration. It is the best fit for applications that need consistent tooltip behavior across many components without reinventing the wheel. Select this when you prefer a declarative API where you link a target ID to a tooltip component rather than managing complex render props or hooks.

README for react-popper-tooltip

react-popper-tooltip

npm version npm downloads codecov

A React hook to effortlessly build smart tooltips. Based on react-popper and popper.js.

NOTE

  • This is the documentation for 4.x which introduced the usePopperTooltip hook.
  • If you're looking for the render prop version, see 3.x docs.
  • If you're looking to upgrade from 3.x render prop to 4.x hook, please refer to our migration guide.

Examples

Installation

You can install react-popper-tooltip with npm or yarn.

npm i react-popper-tooltip
# or
yarn add react-popper-tooltip

Quick start

This example illustrates how to create a minimal tooltip with default settings and using our default CSS file.

import * as React from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import 'react-popper-tooltip/dist/styles.css';

function App() {
  const {
    getArrowProps,
    getTooltipProps,
    setTooltipRef,
    setTriggerRef,
    visible,
  } = usePopperTooltip();

  return (
    <div className="App">
      <button type="button" ref={setTriggerRef}>
        Trigger
      </button>
      {visible && (
        <div
          ref={setTooltipRef}
          {...getTooltipProps({ className: 'tooltip-container' })}
        >
          <div {...getArrowProps({ className: 'tooltip-arrow' })} />
          Tooltip
        </div>
      )}
    </div>
  );
}

render(<App />, document.getElementById('root'));

Styling

With react-popper-tooltip, you can use CSS, LESS, SASS, or any CSS-in-JS library you're already using in your project. However, we do provide a minimal CSS-file file you can use for a quick start or as a reference to create your own tooltip styles.

Import react-popper-tooltip/dist/styles.css to import it into your project. Add classes tooltip-container and tooltip-arrow to the tooltip container and arrow element accordingly.

While the tooltip is being displayed, you have access to some attributes on the tooltip container. You can use them in your CSS in specific scenarios.

  • data-popper-placement: contains the current tooltip placement. You can use it to properly offset and display the arrow element (e.g., if the tooltip is displayed on the right, the arrow should point to the left and vice versa).

  • data-popper-reference-hidden: set to true when the trigger element is fully clipped and hidden from view, which causes the tooltip to appear to be attached to nothing. Set to false otherwise.

  • data-popper-escaped: set to true when the tooltip escapes the trigger element's boundary (and so it appears detached). Set to false otherwise.

  • data-popper-interactive: contains the current interactive option value.

API reference

usePopperTooltip

const {
  getArrowProps,
  getTooltipProps,
  setTooltipRef,
  setTriggerRef,
  tooltipRef,
  triggerRef,
  visible,
  ...popperProps
} = usePopperTooltip(
  {
    closeOnOutsideClick,
    closeOnTriggerHidden,
    defaultVisible,
    delayHide,
    delayShow,
    followCursor,
    interactive,
    mutationObserverOptions,
    offset,
    onVisibleChange,
    placement,
    trigger,
    visible,
  },
  popperOptions
);

Options

  • closeOnOutsideClick: boolean, defaults to true

If true, closes the tooltip when user clicks outside the trigger element.

  • closeOnTriggerHidden: boolean, defaults to false

Whether to close the tooltip when its trigger is out of boundary.

  • delayHide: number, defaults to 0

Delay in hiding the tooltip (ms).

  • delayShow: number, defaults to 0

Delay in showing the tooltip (ms).

  • defaultVisible: boolean, defaults to false

The initial visibility state of the tooltip when the hook is initialized.

  • followCursor: boolean, defaults to false

If true, the tooltip will stick to the cursor position. You would probably want to use this option with hover trigger.

  • mutationObserverOptions: MutationObserverInit | null, defaults to { attributes: true, childList: true, subtree: true }

Options to MutationObserver , used internally for updating tooltip position based on its DOM changes. When the tooltip is visible and its content changes, it automatically repositions itself. In some cases you may need to change which parameters to observe or opt-out of tracking the changes at all.

  • offset: [number, number], defaults to [0, 6]

This is a shorthand for popperOptions.modifiers offset modifier option. The default value means the tooltip will be placed 6px away from the trigger element (to reserve enough space for the arrow element).

We use this default value to match the size of the arrow element from our default CSS file. Feel free to change it if you are using your own styles.

See offset modifier docs.

popperOptions takes precedence over this option.

  • onVisibleChange: (state: boolean) => void

Called with the tooltip state, when the visibility of the tooltip changes.

  • trigger: TriggerType | TriggerType[] | null, where TriggerType = 'click' | 'right-click' | 'hover' | 'focus', defaults to hover

Event or events that trigger the tooltip. Use null if you want to disable all events. It's useful in cases when you control the state of the tooltip.

  • visible: boolean

The visibility state of the tooltip. Use this prop if you want to control the state of the tooltip. Note that delayShow and delayHide are not used if the tooltip is controlled. You have to apply delay on your external state.

react-popper-tooltip manages its own state internally and calls onVisibleChange handler with any relevant changes.

However, if more control is needed, you can pass this prop, and the state becomes controlled. As soon as it's not undefined, internally, react-popper-tooltip will determine its state based on your prop's value rather than its own internal state.

  • placement: 'auto' | 'auto-start' | 'auto-end' | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'right' | 'right-start' | 'right-end' | 'left' | 'left-start' | 'left-end';

The preferred placement of the tooltip. This is an alias for popperOptions.placement option.

popperOptions takes precedence over this option.

  • interactive: boolean, defaults to false

If true, hovering the tooltip will keep it open. Normally, if you trigger the tooltip on hover event, the tooltip closes when the mouse cursor moves out of the trigger element. If it moves to the tooltip element, the tooltip stays open. It's useful if you want to allow your users to interact with the tooltip's content (select and copy text, click a link, etc.). In this case you might want to increase delayHide value to give the user more time to react.

  • popperOptions: { placement, modifiers, strategy, onFirstUpdate }

These options passed directly to the underlying usePopper hook. See https://popper.js.org/docs/v2/constructors/#options.

Keep in mind, if you set placement or any modifiers here, it replaces offset and placement options above. They won't be merged into the final object. You have to add offset modifier along with others here to make it work.

Returns

  • triggerRef: HTMLElement | null

The trigger DOM element ref.

  • tooltipRef: HTMLElement | null

The tooltip DOM element ref.

  • setTooltipRef: (HTMLElement | null) => void | null

A tooltip callback ref. Must be assigned to the tooltip's ref prop.

  • setTriggerRef: (HTMLElement | null) => void | null

A trigger callback ref. Must be assigned to the trigger's ref prop.

  • visible: boolean

The current visibility state of the tooltip. Use it to display or hide the tooltip.

  • getArrowProps: (props) => mergedProps

This function merges your props and the internal props of the arrow element. We recommend passing all your props to that function rather than applying them on the element directly to avoid your props being overridden or overriding the internal props.

It returns the merged props that you need to pass to the arrow element.

  • getTooltipProps: (props) => mergedProps

This function merges your props and the internal props of the tooltip element. We recommend passing all your props to that function rather than applying them on the element directly to avoid your props being overridden or overriding the internal props.

It returns the merged props that you need to pass to tooltip element.

  • popperProps: { update, forceUpdate, state }

Some props returned by the underlying usePopper hook. See https://popper.js.org/react-popper/v2/hook.

This doesn't include styles and attributes props. They are included into getArrowProps and getTooltipProps prop getters.