A highly customizable vanilla JS tooltip & popover library

Tippy's features


The default tippy tooltip looks like this when given no options. It has a nifty backdrop filling animation!



A tooltip can be placed in four different ways in relation to its reference element. Additionally, the tooltip can be shifted using the suffix -start or -end.



Arrows point toward the reference element. There are two different types of arrows: sharp and round. You can transform the proportion and scale of the arrows any way you like.



Triggers define the types of events that cause a tooltip to show.



Tooltips can be interactive, meaning they won't hide when you hover over or click on them.



Tooltips can have different types of animations.



A tippy can have different transition durations.



Tooltips can delay showing or hiding* after a trigger.



Tooltips can contain HTML, allowing you to craft awesome interactive popovers.



A tippy can have any kind of theme you want! Creating a custom theme is a breeze.



Tippy has a ton of features, and it's constantly improving.


You might be wondering why you should use a 15 kB JS library for tooltips and popovers instead of a CSS solution. Pure CSS tooltips are great for simple tooltips when the reference element is positioned in a certain way, but they:

  • Will overflow when the tooltip is large and the reference is close to the window edge
  • Can't flip to stay optimally visible within the viewport
  • Can't follow the mouse cursor
  • Difficult to work with self-closing elements like img
  • JavaScript is required for dynamic HTML content
  • JavaScript is required to perform side effects (e.g. AJAX)

In addition, Tippy automatically handles many use cases available declaratively in a simple option API. Options like followCursor, interactive, touch, arrow, and the on* lifecycle functions make dealing with tooltips & popovers a breeze.


🔗 Option 1: CDN

Include this script from the unpkg CDN in your HTML document before your own scripts:

<script src="https://unpkg.com/tippy.js@3/dist/tippy.all.min.js"></script>

It's recommended to place this at the bottom of the <body>, or in the <head> with a defer attribute.

📦 Option 2: Package Manager

Install using either npm or yarn:

npm i tippy.js
yarn add tippy.js

Then you can import the tippy module:

import tippy from 'tippy.js'

You'll also need to import Tippy's CSS. With a module bundler like Webpack or Parcel, it can be imported directly:

import 'tippy.js/dist/tippy.css'

🎁 View Library Components

If you would like to use Tippy.js as a declarative component, there are wrappers available.

📁 Files

Tippy builds a bunch of different files that can be used:

  • tippy.all.js is all dependencies (Tippy + Popper + CSS) in a single file. The CSS is injected into the document head. Note that themes CSS is not included by default, they must be externally imported or linked. They are located under dist/themes/.
  • tippy.js is Tippy + Popper together, without the CSS.
  • tippy.standalone.js is Tippy by itself, without Popper or the CSS. This is useful if you are using a CDN and want to use the latest version of Popper.js if the bundled version is outdated, or use Popper itself for other things.
  • tippy.css is Tippy's CSS stylesheet by itself.

There are also .min versions of the above, which means the file is minified for production use.

💻 Browser support

Tippy is compatible with browsers with requestAnimationFrame and MutationObserver support (IE11+). This means most browsers from 2013 onwards, about 99% of desktop users and 95% of mobile users globally (mainly due to Opera Mini on mobile not being supported).

IE11 requires a classList polyfill if using an SVG element as the reference.

If you need to support old browsers too, you can set the native title attribute on desktop. On unsupported mobile browsers (such as Opera Mini), it's best to inline the content next to the reference element.

⚠️The code throughout this documentation is making use of new JavaScript features (ES6+) that old browsers don't support (such as IE11). If you're going to copy code from here, make sure to use Babel to transpile it into ES5.


Method 1: Auto

Give your reference element a data-tippy attribute containing the tooltip content.

<button data-tippy="I'm a tooltip!">Text</button>

When Tippy.js is loaded in the document, it will search for elements with the attribute and give them a tooltip automatically. This means you won't have to touch JavaScript at all.

⚠️ The data-tippy attribute only works on initial page load. If you have dynamically generated elements or are using a view library/framework (React, Vue, Angular), use Method 2 below, using data-tippy-content instead of data-tippy.

Method 2: Function

Use the tippy function.

tippy('button', { content: "I'm a tooltip!" })


Using data-tippy-content allows you to use the function for common custom configuration while giving each tooltip different content.

<button class="btn" data-tippy-content="Tooltip A">Text</button>
<button class="btn" data-tippy-content="Tooltip B">Text</button>
<button class="btn" data-tippy-content="Tooltip C">Text</button>

🎛 Accepted inputs

A single DOM Element (or an array of them) will work:


As well as a NodeList:


🤯 Advanced

You can use a virtual element as the positioning reference instead of a real element:

const virtualReference = {
  getBoundingClientRect() {
    return {
      width: 100,
      height: 100,
      top: 100,
      left: 100,
      right: 200,
      bottom: 200
  clientHeight: 100,
  clientWidth: 100

tippy(virtualReference, { content: "I'm a tooltip!" })

Popper.js uses these properties to determine the position of the tooltip.


tippy() takes an object of options as a second argument for you to configure the tooltips being created. Here's an example:

<button class="btn">Text</button>
tippy('.btn', {
  content: "I'm a tooltip!",
  delay: 100,
  arrow: true,
  arrowType: 'round',
  size: 'large',
  duration: 500,
  animation: 'scale'


🏷 Data attributes

You can also specify options on the reference element itself by adding data-tippy-* attributes. This will override the options specified in the instance.

Used in conjunction with the Auto Method, you can give elements custom tooltips without ever touching JavaScript.

  data-tippy="I'm a Tippy tooltip!"


Default config

Use the tippy.setDefaults() method to change the default configuration for tippys. It will apply these settings to every future instance.

  arrow: true,
  arrowType: 'round',
  duration: 0

Note that the auto-initializing function is deferred with setTimeout(), which means you can change the default config before the tooltips are automatically created.


Below is a list of all possible options you can pass to tippy.

a11ytrueBooleanIf true, ensures the reference element can receive focus by adding tabindex="0" if the element is not natively focusable like a button.
allowHTMLtrueBooleanDetermines if HTML can be rendered in the tippy.
animateFilltrueBooleanDetermines if the tippy's background fill should be animated. Disabled if arrow: true.
The type of transition animation.
The element to append the tippy to. Use a function that returns an element to dynamically append the tippy relative to the reference element.
tippy(list, {
  appendTo(ref) {
    return ref.parentNode
arrowfalseBooleanDetermines if an arrow should be added to the tippy pointing toward the reference element.
The type of arrow. "sharp" is a CSS triangle using the border method, while "round" is a custom SVG shape.
CSS transform to apply to the arrow. Only scale and translate are supported. It is dynamic. Apply the transform that you would normally give to a "top" placement, even if the placement is different.
The content of the tippy.
delay[0, 20]
[show, hide]
Delay in ms once a trigger event is fired before a tippy shows or hides. Use an array of numbers such as [100, 500] to specify a different value for show and hide. Use null in the array to use the default value, e.g. [null, 50].
duration[275, 250]
[show, hide]
Duration of the CSS transition animation in ms. Use an array of numbers such as [100, 500] to specify a different value for show and hide. Add null in the array to use the default value, e.g. [null, 50].
distance10NumberHow far in pixels the tippy element is from the reference element. Only applies to a single axis and not to the parent popper element, see offset.
fliptrueBooleanDetermines if the tippy flips so that it is placed within the viewport as best it can be if there is not enough room.
Determines the order of flipping, i.e. which placements to prefer if a certain placement cannot be used. Use an array such as ["bottom", "left"] to prefer the "left" placement if "bottom" is unavailable. By default, it chooses the opposite axis, i.e. "top".
Determines if the tippy follows the user's mouse cursor while showing. Use the strings "vertical" or "horizontal"to only follow the cursor on a single axis.
Determines if the tippy should hide if its reference element was clicked. For click-triggered tippys, using false will prevent the tippy from ever hiding once it is showing. To prevent clicks outside of the tippy from hiding it but still allow it to be toggled, use the string "toggle".
inertiafalseBooleanAdds an attribute to the tippy element that changes the CSS transition timing function to add an inertial "slingshot" effect to the animation.
interactivefalseBooleanDetermines if a tippy should be interactive, i.e. able to be hovered over or clicked without hiding.
interactiveBorder2NumberDetermines the size of the invisible border around a tippy that will prevent it from hiding (only relevant for the hover trigger). Useful to prevent the tippy from accidentally hiding from clumsy cursor movements.
interactiveDebounce0NumberA number in ms that debounces the onMouseMove handler which determines when the tippy should hide.
lazytrueBooleanBy default, the popperInstance (the positioning engine for the tippy) is lazily created. That is, it's only created when necessary (i.e. triggering the tippy for the first time). Setting this prop to false allows you to access the instance synchronously without needing to show the tippy first.
livePlacementtrueBooleanDetermines if the popper instance should listen to scroll events. This means it will update the position on scroll. If you don't want the tippy to flip around when scrolling, and the tippy's reference is not in a scrollable container, you can set this to false.
Specifies the maximum width of the tippy. Specifying a number will automatically append px. If using a string, ensure you add units (such as rem).
multiplefalseBooleanDetermines if the reference can have multiple tippy instances.
offset0NumberAn offset that Popper.js uses to offset the popper element. Can work with both the x and y axis, distinct from distance.
onHiddennoopFunctionLifecycle function invoked when the tippy has fully transitioned out.
onHidenoopFunctionLifecycle function invoked when the tippy begins to transition out.
onMountnoopFunctionLifecycle function invoked when the tippy has been mounted to the DOM.
onShownoopFunctionLifecycle function invoked when the tippy begins to transition in.
onShownnoopFunctionLifecycle function invoked when the tippy has fully transitioned in.
performancefalseBooleanIf true, disables data-tippy-* attributes which reduces init execution by half.
Positions the tippy relative to its reference element. Use the suffix -start or -end to shift the tippy to the start or end of the reference element, instead of centering it. For example, top-start or left-end.
popperOptions{}ObjectSpecify custom Popper.js options. See the Popper.js documentation for more.
shouldPopperHideOnBlur(FocusOutEvent) => trueFunctionA function that returns a boolean to determine if the popper element should hide if it's blurred (applies only if interactive). If the popper element is blurred (i.e. no elements within it are in focus), the popper is hidden. However, there are cases in which you may need to keep it visible even when not in focus.
showOnInitfalseBooleanIf true, the tooltip will be shown immediately once the instance is created. If using on page load, use sticky: true because the reference element can move around while the layout gets built by the browser after initialization (unless the layout is guaranteed to be static).
The size of the tippy.
stickyfalseBooleanEnsures the tippy stays stuck to its reference element if it moves around while showing.
target""StringCSS selector used for event delegation.
Themes added as classes (each separated by a space) to the tippy's class list, which adds a -theme suffix, i.e. "dark-theme". Note that the themes apart from the default "dark" theme are not included in the main CSS by default, they must be imported/linked separately.
touchtrueBooleanDetermines if the tippy displays on touch devices.
touchHoldfalseBooleanDetermines trigger behavior on touch devices. Instead of a tap on the reference to show and a tap elsewhere to hide the tippy, the reference must be pressed and held for the tippy to show. Letting go from the screen will hide it. To prevent the mobile context menu from appearing, ensure the element cannot be selected using user-select: none; and/or prevent the default behavior for the contextmenu event.
trigger"mouseenter focus"
The events (each separated by a space) which cause a tippy to show. Use manual to only trigger the tippy programmatically.
updateDuration200NumberThe transition duration between position updates for the sticky option. Set to 0 to prevent flips from transitioning.
waitnullFunctionA function that, when defined, will wait until you manually invoke show() when a tippy is triggered. It takes the tippy instance and the trigger event as arguments.
tippy(ref, {
  wait(tip, event) {
    // Delay by 200ms if
    // trigger was not focus
      event.type === 'focus'
        ? 0
        : 200
zIndex9999NumberThe z-index of the popper element.

🌐 AJAX tooltips

Lifecycle functions allow you to do powerful things with tippys. Here's an example of dynamic content which on show, fetches a new random image from the Unsplash API.


const INITIAL_CONTENT = 'Loading...'

const state = {
  isFetching: false,
  canFetch: true

tippy('#ajax-tippy', {
  async onShow(tip) {
    if (state.isFetching || !state.canFetch) return

    state.isFetching = true
    state.canFetch = false

    try {
      const response = await fetch('https://unsplash.it/200/?random')
      const blob = await response.blob()
      const url = URL.createObjectURL(blob)
      if (tip.state.isVisible) {
        const img = new Image()
        img.width = 200
        img.height = 200
        img.src = url
    } catch (e) {
      tip.setContent(`Fetch failed. ${e}`)
    } finally {
      state.isFetching = false
  onHidden(tip) {
    state.canFetch = true

Note that if you don't specify the dimensions of the image (width and height), the tooltip will be positioned incorrectly once it loads. This is because the position of the tooltip is updated before the image's dimensions become known by the browser.

Improved animation with a height transition


See the CodePen demo.

📡 Event delegation

Event delegation only requires minimal setup. Your setup should look similar to this, with a parent element wrapping the child elements you would like to give tooltips to:

<div id="parent">
  <div class="child">Text</div>
  <div class="child">Text</div>
  <div class="child">Text</div>
  <div class="other">Text</div>

Then, specify a CSS selector as the target that matches child elements which should receive tooltips

tippy('#parent', {
  content: 'Shared content',
  target: '.child'

⚠️Avoid binding a Tippy instance to the body, as mouseover / mouseout events will constantly fire as the cursor moves over the page. Instead, give it to the nearest possible parent element.

Tooltips inside a scrollable container

Add the following options to prevent the tippy from staying stuck within the viewport.

tippy(ref, {
  popperOptions: {
    modifiers: {
      preventOverflow: {
        boundariesElement: 'window'

Hiding tooltips on scroll

In some cases it may be desirable to hide tooltips when scrolling (for example, on touch devices).

window.addEventListener('scroll', () => tippy.hideAllPoppers())

Cancel tooltips from showing or hiding

If you return false in the onShow or onHide lifecycle function, it will cancel the operation. Note that this is synchronous, so it won't wait for an AJAX request, etc.

const template = document.querySelector('#myTemplate')
tippy(ref, {
  content: template,
  onShow() {
    // Don't show if the tippy content contains a <strong> element.
    if (template.querySelector('strong')) {
      return false
  onHide({ props }) {
    // Don't hide if the tooltip has an arrow.
    if (props.arrow) {
      return false

Buttons with tooltips on touch devices

A tooltip on a button is generally used to convey information before the user decides to click on it. On touch devices, this isn't possible because a tap is required to show the tooltip, which will fire a click event.

On iOS, a tap will show the tooltip but click events won't fire until a second tap. This allows the user to see the tooltip before deciding to click the button. On Android, clicking the button will show the tooltip and also fire a click event.

Depending on your use case, one of these will be preferred, so user agent checking may be needed. If neither behavior is preferred, consider using the touchHold: true option which allows the user to see the tooltip while pressing and holding the button, but won't fire a click event unless the click appears to be intentional.

const button = document.querySelector('button')
const isIOS = /iPhone|iPad|iPod/.test(navigator.platform)

Make iOS behave like Android (single tap to click)
button.addEventListener('click', () => {
  // Your logic
tippy(button, {
  onShow() {
    if (isIOS) {

Make Android behave like iOS (double tap to click)
// Useful function for dynamically determining the input type:
// https://github.com/30-seconds/30-seconds-of-code#onuserinputchange
let isUsingTouch = false
onUserInputChange(type => {
  isUsingTouch = type === 'touch'

const tip = tippy.one(button)
button.addEventListener('click', () => {
  if (isIOS || !isUsingTouch ? true : tip.state.isShown) {
    // Your logic

When using Tippy.js, there are two types of objects to think about: collections and instances.


Whenever you call tippy(), you are potentially creating many tippys at once. It returns an object containing information about the tippys you created.

const tipCollection = tippy('.btn')

tipCollection is a plain object.

  // Targets that should receive a tippy
  targets: '.btn',

  // Default props + options merged together
  props: { ... },

  // Array of all instances that were created
  instances: [tip, tip, tip, ...],

  // Method to destroy all the tooltips that were created
  destroyAll() { ... }

Tippy instances

Stored on reference elements via the _tippy property, and inside the instances array of the collection.

const btn = document.querySelector('.btn')
const tip = btn._tippy

Alternatively, you can use the tippy.one() method to return the instance directly, because only a single tippy is created.

const tip = tippy.one('.btn')

tip is also a plain object.

  // id of the instance (1 to Infinity)
  id: 1,

  // Reference element that is the trigger for the tooltip
  reference: Element,

  // Popper element that contains the tooltip
  popper: Element,

  // Object that contains the child elements of the popper element
  popperChildren: { ... },

  // Popper instance is not created until shown for the first time,
  // unless specified otherwise
  popperInstance: null,

  // Instance props + attribute options merged together
  props: { ... },

  // The state of the instance
  state: {
    // Has the instance been destroyed?
    isDestroyed: false,
    // Is the instance enabled?
    isEnabled: true,
    // Is the tooltip currently visible and not transitioning out?
    isVisible: false,
    // Is the tooltip currently mounted to the DOM?
    isMounted: false,
    // Is the tooltip currently fully showing and not transitioning out or in?
    isShown: false

  // Also contains methods, which you'll learn in the next section


There are a couple of shortcuts available for accessing the instance.

// The popper element has the instance attached to it:
// As does the reference element (as seen above):

Tippy instances have 7 methods available which allow you to control the tooltip without the use of UI events.

<button data-tippy="Hello">Text</button>
const btn = document.querySelector('button')

The Tippy instance is stored on the button element via the _tippy property.

const tip = btn._tippy
Why is it prefixed with an underscore? Since we're attaching a non-standard property to an Element, we prefix it with an underscore. In the future, there may exist a real tippy property of elements that would get overwritten by the library, and real DOM properties are never prefixed with an underscore.

Show the tooltip


Hide the tooltip


Custom transition duration

Pass a number in as an argument to override the instance option:


Disable the tooltip

The tooltip can be temporarily disabled from showing/hiding:


To re-enable:


Destroy the tooltip

To permanently destroy the tooltip and remove all listeners from the reference element:


The _tippy property is deleted from the reference element upon destruction.

Update the tooltip

Pass an object of new props to the set() method to update the tooltip. The tooltip element will be redrawn to reflect the change.

  content: 'New content',
  arrow: true,
  duration: 1000,
  animation: 'perspective'

Update the tooltip content

There is a shortcut for directly updating the tooltip content.

tip.setContent('New content')

Static methods

There are a few static methods on the tippy function itself. These methods are global and do not affect a single instance.

Hide all visible poppers on the page:


Disable animation-related default props:


Set the default props for each new tippy instance:


Create a single tooltip and return the instance directly:

tippy.one(reference, options)

Along with using a string of HTML content, you can provide an HTMLElement for the content option.

<button class="btn">Text</button>
<div id="myTemplate">
  My HTML <strong style="color: pink;">tooltip</strong> content
tippy('.btn', {
  content: document.querySelector('#myTemplate')


Tippy will append the DOM element directly to the tooltip, so it will be removed from the page.

To reuse the template multiple times, you can pass its innerHTML content:

const html = document.querySelector('#myTemplate').innerHTML
tippy('button', { content: html })

If each reference element should have a different template associated with it, you can pass a function that receives the current reference as an argument and return its associated template:

<button data-template="template1">Text</button>
<div id="template1">Template 1</div>

<button data-template="template2">Text</button>
<div id="template2">Template 2</div>
tippy('button', {
  content(reference) {
    return document.getElementById(reference.getAttribute('data-template'))

Tippy element structure

To know what selectors to use, it's helpful to understand the structure of a tippy element.

<div class="tippy-popper" x-placement="top">
  <div class="tippy-tooltip">
    <div class="tippy-content">
      My content

A tippy is essentially three nested divs.

  • tippy-popper is what Popper.js uses to position the tippy. You shouldn't apply any styles directly to this element, but you will need it when targeting a specific placement (x-placement).
  • tippy-tooltip is the actual tooltip. Use this to style the tooltip when animateFill: false.
  • tippy-backdrop is the background fill of the tooltip. Use this when animateFill: true.
  • tippy-content is anything inside the tooltip.

However, depending on the options you supply, additional elements may exist inside it, such as an arrow or backdrop (default) element.

<div class="tippy-popper" x-placement="top">
  <div class="tippy-tooltip">
    <div class="tippy-backdrop"></div> <!-- animateFill: true -->
    <div class="tippy-arrow"></div> <!-- arrow: true -->
    <div class="tippy-content">
      My content

Creating a theme

If you wanted to make a theme called honeybee, then your CSS would look like:

/* If `animateFill: true` (default) */
.tippy-tooltip.honeybee-theme .tippy-backdrop {
  background-color: yellow;
  font-weight: bold;
  color: #333;

/* If `animateFill: false` */
.tippy-tooltip.honeybee-theme {
  background-color: yellow;
  border: 2px solid orange;
  font-weight: bold;
  color: #333;

The -theme suffix is required.

To apply the theme to the tooltip, specify a theme option without the -theme suffix:

tippy('.btn', {
  theme: 'honeybee'


Styling the arrow

There are two arrow selectors: .tippy-arrow and .tippy-roundarrow. The first is the pure CSS triangle shape, while the second is a custom SVG.

You will need to style the arrow for each different popper placement if using the default (sharp) arrow: top, bottom, left, right.

/* Default (sharp) arrow */
.tippy-popper[x-placement^='top'] .tippy-tooltip.light-theme .tippy-arrow {
  border-top-color: #fff;
.tippy-popper[x-placement^='bottom'] .tippy-tooltip.light-theme .tippy-arrow {
  border-bottom-color: #fff;
.tippy-popper[x-placement^='left'] .tippy-tooltip.light-theme .tippy-arrow {
  border-left-color: #fff;
.tippy-popper[x-placement^='right'] .tippy-tooltip.light-theme .tippy-arrow {
  border-right-color: #fff;
/* Round arrow */
.tippy-tooltip.light-theme .tippy-roundarrow {
  fill: #fff;