Skip to content

Misc

Groups

Groups are designed to improve user experience when tooltips have delays. When moving the cursor between each item in the group, the tooltips will instantly show.

Compare the following tooltips without grouping:

tippy('button', { delay: 1000 })

Now compare the following tooltips with grouping:

tippy.group(tippy('button', { delay: 1000 }))

As you can see, the user experience is much nicer. To enable group behavior, call the method tippy.group() and pass in an array of Tippy instances.

The group method also takes an optional options object as a second argument:

tippy.group(instances, {
  delay: 1000, // if the instances don't specify a `delay`
  duration: 50, // instead of 0 (instant duration)
})

Anchor links

When an inline anchor link spans two or more lines, the tippy gets centered as though it were attached to a block:

If this is a problem (e.g. with interactivity), there is a way to solve it:

It requires using a virtual positioning reference. See the CodePen demo.

Scrollable containers

Here are 3 common solutions. Note that no solution is completely ideal in all situations - it depends on the UI. In each of these, if you want the tippy to be clipped by the scrolling area, use appendTo: "parent". If the tippy is large then this may be undesirable.

Unintrusive

With this technique, the tooltip acts like it does on the document normally. It gets clipped by the scrolling area and does not attempt to position itself to be fully visible (but will still flip).

tippy('#unintrusive-example', {
  boundary: 'window',
  appendTo: 'parent',
  popperOptions: {
    modifiers: {
      flip: {
        boundariesElement: 'scrollParent',
      },
    },
  },
})
Flip on update

With this technique, the tooltip attempts to reposition itself to be visible to the user at all times. Sometimes this can be a little overly intrusive, though.

tippy('#scroll-flipping-example', {
  appendTo: 'parent',
  flipOnUpdate: true,
  // Leave this out if you want flipping to occur based on the
  // viewport rather than the container
  popperOptions: {
    modifiers: {
      flip: {
        boundariesElement: 'scrollParent',
      },
    },
  },
})
Hide on scroll

With this technique, the tooltip hides instantly whenever the user starts scrolling. In addition, it initially positions itself to be as visible as possible within the scrolling area.

const instance = tippy(document.querySelector('#hide-on-scroll-example'))
const container = document.querySelector('#scrollable-container')
container.addEventListener('scroll', () => {
  instance.hide(0)
})

Different trigger target

You may want the tippy to appear at a different location from its trigger (event listeners) target. For example:

Trigger target vs

For this, you can utilize the triggerTarget option:

const innerOrangeSpanElement = document.querySelector('span')
const outerDivElement = document.querySelector('div')

tippy(innerOrangeSpanElement, {
  triggerTarget: outerDivElement,
})

Custom animations

Custom animations can be created, but require a bit of CSS to setup. The default "shift-away" animation has a hardcoded translation of 10px. Sometimes, for small elements, this can be a bit much, especially if the tippy has an arrow.

You need to:

  • Target each different basic placement in x-placement: top, bottom, left, right
  • Target the two visibility states in data-state: visible and hidden
  • Use your own animation name in data-animation

Using a preprocessor with nesting support (e.g. SASS) helps reduce the boilerplate and amount of code required.

/* Subtle shift-away animation */

/* Top */
.tippy-tooltip[x-placement^='top'][data-animation='subtle-shift-away'][data-state='visible'] {
  transform: translateY(-10px);
  opacity: 1;
}
.tippy-tooltip[x-placement^='top'][data-animation='subtle-shift-away'][data-state='hidden'] {
  transform: translateY(-5px);
  opacity: 0;
}

/* Bottom */
.tippy-tooltip[x-placement^='bottom'][data-animation='subtle-shift-away'][data-state='visible'] {
  transform: translateY(10px);
  opacity: 1;
}
.tippy-tooltip[x-placement^='bottom'][data-animation='subtle-shift-away'][data-state='hidden'] {
  transform: translateY(5px);
  opacity: 0;
}

/* Left */
.tippy-tooltip[x-placement^='left'][data-animation='subtle-shift-away'][data-state='visible'] {
  transform: translateX(-10px);
  opacity: 1;
}
.tippy-tooltip[x-placement^='left'][data-animation='subtle-shift-away'][data-state='hidden'] {
  transform: translateX(-5px);
  opacity: 0;
}

/* Right */
.tippy-tooltip[x-placement^='right'][data-animation='subtle-shift-away'][data-state='visible'] {
  transform: translateX(10px);
  opacity: 1;
}
.tippy-tooltip[x-placement^='right'][data-animation='subtle-shift-away'][data-state='hidden'] {
  transform: translateX(5px);
  opacity: 0;
}

Why do the attribute names have a different prefix (x-* and data-*)?

Popper.js (v1) uses the x- prefix convention, while Tippy.js adheres to the HTML5 spec with data- prefix. Popper.js v2 will change the prefix to match Tippy's.

Animate.css animations

Want more animations than the built-in ones? You can use any CSS animation you like, for example, animate.css. Include it on your page and then add/remove the classes onMount/onHidden:

tippy('button', {
  animation: 'fade',
  // If you don't want the filling effect as well
  animateFill: false,
  // The default CSS uses `transform: translateY(10px)`, but animated.css transforms
  // will override it, so we'll need to use +10 more distance than the default (10)
  distance: 20,
  onMount(instance) {
    const { tooltip } = instance.popperChildren
    requestAnimationFrame(() => {
      tooltip.classList.add('animated')
      tooltip.classList.add('wobble')
    })
  },
  onHidden(instance) {
    const { tooltip } = instance.popperChildren
    tooltip.classList.remove('animated')
    tooltip.classList.remove('wobble')
  },
})

Event delegation

Event delegation allows you to bind a Tippy instance to a parent container element to handle creation of tippys for children.

This allows two things:

  • It eliminates the need to create new instances for new children getting appended to the parent.
  • It improves performance as only a single instance is created, and event bubbling handles creation of instances at the right time.

Children will inherit the options specified in the parent instance, but individual options can be specified via data-tippy-* attributes.

<div id="parent">
  <button 
    class="child" 
    data-tippy-content="Tooltip 1"
  >
    One
  </button>
  <button 
    class="child" 
    data-tippy-content="Tooltip 2"
    data-tippy-arrow="true"
  >
    Two
  </button>
  <button 
    class="child" 
    data-tippy-content="Tooltip 3" 
    data-tippy-theme="light"
  >
    Three
  </button>
</div>

To enable this behavior, bind the instance to the #parent element and specify a target option with a CSS selector that matches the child elements which should receive tooltips:

tippy('#parent', {
  target: '.child',
})

Hide tooltips on scroll

window.addEventListener('scroll', () => {
  // If you want to use their own durations
  tippy.hideAll()
  // Or, if you want to make them all hide instantly
  tippy.hideAll({ duration: 0 })
})

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 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)

/*========================================================
A: Use `touchHold: true`
========================================================*/
tippy(button, {
  touchHold: true,
})

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

/*========================================================
C: 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 instance = tippy(button)

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

onMouseRest implementation

You might desire to use this functionality instead of using a delay. Instead, the tippy will only show once the cursor is resting over the element, but not while it continues to move around over it.

const button = document.querySelector('button')
const instance = tippy(button, {
  trigger: 'focus',
})

let timeout

button.addEventListener('mousemove', () => {
  clearTimeout(timeout)
  timeout = setTimeout(() => instance.show(), 125)
})

button.addEventListener('mouseleave', () => {
  clearTimeout(timeout)
  instance.hide()
})

Virtual elements

In some cases you may need to use a virtual element instead of a real DOM element. Pass a plain object with a getBoundingClientRect method in:

tippy({
  getBoundingClientRect: () => ({
    width: 0,
    height: 0,
    top: 100,
    right: 100,
    bottom: 100,
    left: 100,
  }),
})

Change the numbers to suit your needs. Popper.js uses getBoundingClientRect() to position the tippy.