# easy-tooltips

A lightweight, zero-dependency tooltip library using modern JavaScript and CSS.  
Just add `data-easy-tooltip` to any element! No setup or config required.

[![npm version](https://badge.fury.io/js/easy-tooltips.svg)](https://www.npmjs.com/package/easy-tooltips)
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/easy-tooltips/badge)](https://www.jsdelivr.com/package/npm/easy-tooltips)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

[**Live Demo**](https://easy-tooltips.ewanhowell.com/)

## Features

* No dependencies
* Works with mouse, touch, and keyboard focus
* Customizable via CSS variables
* Automatically repositions and shifts to fit the screen
* Smooth, non-interrupting animations with smart skip-delay between adjacent tooltips
* Anchor to the trigger element, the cursor, or where the user first hovered
* Plain text or arbitrary HTML content
* Seamless body + arrow drawn as a single SVG path
* Compatible with Vue, React, Svelte, and more

## Quick Start

### Install via npm
```bash
npm install easy-tooltips
```

```js
import "easy-tooltips/styles.css"
import "easy-tooltips"
```

### Or use via CDN
https://www.jsdelivr.com/package/npm/easy-tooltips

### Add tooltips to your HTML
```html
<button data-easy-tooltip="Click to save your changes">Save</button>
<span data-easy-tooltip="This field is required">Username *</span>
<div data-easy-tooltip="Multi-line tooltips&#10;are supported too">Info</div>
```

Multi-line text uses `&#10;` (newline). For full HTML content see [Custom HTML](#custom-html) below.

No additional setup is needed for Vue, React, Svelte, or other frameworks! Tooltips automatically update when the element updates!

Tooltips also show on keyboard focus. Natively focusable elements (buttons, links, inputs) work automatically; for other elements (such as a `<div>` or `<span>`), add `tabindex="0"` so they can receive focus.

## Advanced Usage

### Custom HTML
You can render custom HTML inside a tooltip using `data-easy-tooltip-src`. The value can be a CSS selector or the literal keyword `next`.

The matched element's content is copied into the tooltip.

**Using a CSS selector**
Point to any element in the document. The value is matched by id first, then as a CSS selector, so `tip-shipping` and `#tip-shipping` both work.
```html
<button data-easy-tooltip-src="#tip-shipping">Shipping info</button>
<template id="tip-shipping">
  <strong>Free shipping</strong> on orders over £50<br>
  Delivered in 2 to 4 working days
</template>
```

**Using `next`**
Use `next` to automatically pull content from the next DOM element.
The source element is automatically hidden.
```html
<button data-easy-tooltip-src="next">Ingredients</button>
<div>
  <ul>
    <li>Oats</li>
    <li>Honey</li>
    <li>Sea salt</li>
  </ul>
</div>
```

### Custom tooltip classes
For styling specific tooltips, add `data-easy-tooltip-class`. The value is applied as a class on the generated tooltip:
```html
<button data-easy-tooltip="Saved!" data-easy-tooltip-class="success-tooltip">Save</button>
<button data-easy-tooltip="This cannot be undone!" data-easy-tooltip-class="danger-tooltip bold-tooltip">Delete</button>
```

```css
.success-tooltip {
  --easy-tooltip-background-color: #f0fdf4;
  --easy-tooltip-border-color: #27ae60;
  --easy-tooltip-text-color: #27ae60;
}

.danger-tooltip {
  --easy-tooltip-background-color: #fef2f2;
  --easy-tooltip-border-color: #e74c3c;
  --easy-tooltip-text-color: #e74c3c;
}

.bold-tooltip {
  font-weight: bold;
}
```

### Preferred side
Use `data-easy-tooltip-prefer` to control which side a tooltip shows on. It still flips to the opposite side when there isn't room. Using `left` or `right` switches the tooltip to horizontal mode.

* `above` (default): show above, fall back to below
* `below`: show below, fall back to above
* `left`: show to the left, fall back to the right
* `right`: show to the right, fall back to the left

```html
<button data-easy-tooltip="Shows below" data-easy-tooltip-prefer="below">Hover me</button>
<button data-easy-tooltip="Shows on the right" data-easy-tooltip-prefer="right">Hover me</button>
```

### Anchor modes
By default a tooltip is anchored to its trigger element. Use `data-easy-tooltip-anchor` to change the anchor point:

* `element` (default): anchored to the trigger's bounding box.
* `cursor`: anchored to the cursor (or touch point), follows the cursor as it moves around the element. On touch, the tooltip appears at the tap point and tracks the finger if you drag.
* `pin`: anchored to the first cursor or touch point on entry, then frozen there. Continues tracking the page as you scroll so the anchor stays at the same content position.

```html
<div data-easy-tooltip="I follow your cursor" data-easy-tooltip-anchor="cursor">…</div>
<div data-easy-tooltip="I stay where you first hovered" data-easy-tooltip-anchor="pin">…</div>
```

Keyboard focus has no coordinates and automatically falls back to element anchoring.

## Customization

You can style tooltips using CSS variables (recommended) or by targeting the tooltip classes directly. Note that some CSS variables are required for proper positioning:

```css
:root {
  /* Tooltip appearance */
  --easy-tooltip-background-color: #fff;     /* Background color */
  --easy-tooltip-text-color: #000;           /* Text color */
  --easy-tooltip-border-color: #aaa;         /* Border color */
  --easy-tooltip-border-size: 1px;           /* Border thickness */
  --easy-tooltip-border-radius: 4px;         /* Corner radius of the tooltip */
  --easy-tooltip-padding: 8px 12px;          /* Inner padding */
  --easy-tooltip-max-width: 100%;            /* Maximum tooltip width */

  /* Positioning */
  --easy-tooltip-distance: 16px;             /* Distance from trigger element */
  --easy-tooltip-viewport-padding: 16px;     /* Minimum distance from screen edges */
  --easy-tooltip-arrow-size: 16px;           /* Arrow size (height defaults to width / 2; 0 to disable) */
  --easy-tooltip-arrow-size: 16px 8px;       /* Or specify arrow width and height separately */
  --easy-tooltip-arrow-edge-buffer-x: 12px;  /* Minimum arrow gap from where the border radius starts (for above or below tooltips) */
  --easy-tooltip-arrow-edge-buffer-y: 6px;   /* Minimum arrow gap from where the border radius starts (for left or right tooltips) */
  --easy-tooltip-arrow-radius: 0;            /* Border radius of the arrow tip */

  /* Animation */
  --easy-tooltip-animation-length: 0.15s;    /* Duration of fade animation */
  --easy-tooltip-animation-distance: 4px;    /* Distance the tooltip slides in */
  --easy-tooltip-delay: 0s;                  /* Base delay before the tooltip shows; always added */
  --easy-tooltip-inactive-delay: 0.15s;      /* Extra delay when no tooltip was recently active; drops to 0 once a tooltip is showing */
  --easy-tooltip-cooldown: 0.15s;            /* How long after the last tooltip closes before the inactive-delay applies again */
}
```

### Show delay and quick-switch
Easy-tooltips uses a two-part delay so that the first tooltip waits, but switching between adjacent tooltips feels instant:

* On the first hover, the tooltip waits `delay + inactive-delay` (`0 + 0.15s` by default) before showing. This protects against accidental hovers.
* Once a tooltip has fully appeared, all subsequent tooltips skip the inactive-delay and show in just `delay` (`0s` by default, i.e. instantly).
* When all tooltips have been closed for `cooldown` (`0.15s` by default), behaviour resets to "first hover" again.

Each part is its own variable so you can tune them independently. For example, a slow, deliberate tooltip with a long initial wait but instant skip:

```css
.slow-tooltip {
  --easy-tooltip-inactive-delay: 1s;
}
```

Or a permanent delay regardless of recent activity (e.g. a 500ms reveal on every tooltip):

```css
.always-slow-tooltip {
  --easy-tooltip-delay: 500ms;
}
```

### Advanced customization

The tooltip body and arrow are drawn as a single SVG path, so anything beyond the variables can be done by targeting the path directly with SVG-flavored CSS. `.easy-tooltip-bg` is the SVG element; `.easy-tooltip-bg path` is the path that draws the outline and fill.

#### Borders

The border is the SVG path's `stroke`, so any stroke property works:

```css
/* Dashed border */
.dashed-tooltip .easy-tooltip-bg path {
  stroke-dasharray: 5 3;
  stroke-linecap: round;
}
```

#### Backgrounds

`fill` on the SVG path only accepts a `<paint>` value, so CSS gradients (`linear-gradient`, etc.) won't apply directly. To use a gradient (or any other SVG paint server), declare it once in the document and reference it by id:

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <linearGradient id="brand-gradient" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#3498db"/>
      <stop offset="100%" stop-color="#9b59b6"/>
    </linearGradient>
  </defs>
</svg>
```

```css
.gradient-tooltip .easy-tooltip-bg path {
  fill: url(#brand-gradient);
}
```

The same approach works for `stroke` (gradient borders), and for radial, conic, or pattern paint servers.

For a drop shadow that follows the full body + arrow shape, apply a filter to the SVG element:

```css
.shadow-tooltip .easy-tooltip-bg {
  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.25));
}
```

## How it works

Easy-tooltips uses a smart positioning system that:

1. **Picks a side** - Shows on the preferred side (above by default, or below/left/right) and flips to the opposite side when there isn't room
2. **Keeps it on screen** - Shifts the tooltip along its edge (horizontally for above/below, vertically for left/right) so it stays within the viewport while the arrow keeps pointing at the element
3. **Falls back gracefully** - When a tooltip can't fit on either side, it pins inside the viewport instead of overflowing
4. **Manages animations** - Queues tooltip updates to prevent conflicts and flicker on rapid hover
5. **Skips the delay when grazing** - The first hover waits a short delay to ignore accidental movement, but once any tooltip is showing, switching to adjacent tooltips is instant until you stop hovering for the cooldown period
6. **Stacks newest on top** - When multiple tooltips are visible at once, the most recently activated one renders above the others
7. **Cleans up** - Removes a tooltip automatically when its trigger element leaves the DOM

## License

MIT © [Ewan Howell](https://ewanhowell.com/)