# Tooltip Optimization Patterns: Skip Delay on Subsequent Hovers with the Skills Design System

> Optimize tooltips with Skills design system. Skip hover delay on subsequent hovers for instant feedback, improving user experience and performance.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: best-practices
- Published: 2026-08-02

---

**The Skills design system implements a tooltip pattern that applies an initial delay to prevent accidental pop‑ups, then removes all delay and animation for subsequent hovers using a `data-instant` attribute that sets `transition-duration` to `0ms`.**

The **Skills** repository by Emil Kowalski is a curated knowledge‑base‑as‑code collection of design‑engineering best practices. Each skill lives as a self‑contained Markdown file under `skills/*/SKILL.md`, encoding reusable UI heuristics that teams can reference for consistent, performant interactions. The **tooltip optimization pattern**—skipping delay on subsequent hovers—is codified in the core `emil-design-eng` skill as a concrete application of the system's broader animation philosophy.

## Why Tooltip Optimization Matters

Tooltips present a classic UX tension: users need **discovery help** for icon buttons, but aggressive pop‑ups create **accidental noise** when the cursor passes through. The Skills system solves this with a **two‑phase behavioral model**:

1. **First hover**: Delayed appearance (typically 150–300 ms) to confirm intentional interaction
2. **Subsequent hovers**: Instant, unanimated display for rapid toolbar scanning

This pattern is explicitly defined in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) lines 47–66 as "Tooltips: skip delay on subsequent hovers."

## The Implementation: CSS‑First State Management

The Skills approach uses **attribute‑driven CSS** rather than JavaScript animation, following the system's rule to "use CSS transitions over keyframes for interruptible UI" (lines 69–71).

### Base Transition Setup

```css
/* Base tooltip transition in skills/emil-design-eng/SKILL.md */
.tooltip {
  transition: transform 125ms ease-out, opacity 125ms ease-out;
  transform-origin: var(--transform-origin);
}

/* Hidden state for entering tooltip */
.tooltip[data-starting-style],
.tooltip[data-ending-style] {
  opacity: 0;
  transform: scale(0.97);
}

```

The `transition` property is restricted to **`transform` and `opacity`**—the only properties that bypass layout and paint costs, adhering to the performance rule in lines 78–81.

### The Critical Optimization: Instant Mode

```css
/* Instantly show subsequent tooltips */
.tooltip[data-instant] {
  transition-duration: 0ms;   /* <-- skips delay + animation */
}

```

The `data-instant` attribute is the core mechanism. When present, the transition collapses to zero duration, making the tooltip appear **immediately without motion**.

## JavaScript State Tracking

The implementation requires minimal JavaScript to track whether the user has seen a tooltip during the current session:

```js
/* Tooltip controller pattern from the Skills system */
let hasShownTooltip = false;
const tooltip = document.getElementById('tooltip');

function showTooltip(target, content) {
  // Position and populate
  tooltip.textContent = content;
  const rect = target.getBoundingClientRect();
  tooltip.style.setProperty('--transform-origin', `${rect.left + rect.width / 2}px ${rect.top}px`);
  tooltip.style.left = `${rect.left + rect.width / 2}px`;
  tooltip.style.top = `${rect.bottom + 4}px`;

  // Apply instant flag after first tooltip
  if (hasShownTooltip) {
    tooltip.setAttribute('data-instant', '');
  } else {
    tooltip.removeAttribute('data-instant');
    hasShownTooltip = true;
  }

  // Reveal
  tooltip.style.opacity = '1';
  tooltip.style.transform = 'scale(1)';
}

```

The `hasShownTooltip` boolean triggers the mode switch. Once `true`, all subsequent tooltips receive `data-instant`, eliminating both the **entrance delay** and the **scale/opacity animation**.

## Complete Working Example

Here is a production‑ready implementation combining the Skills pattern with proper event delegation:

```html
<!-- HTML structure -->
<nav class="toolbar">
  <button class="icon-btn" aria-label="Notifications" data-tooltip="Notifications">🔔</button>
  <button class="icon-btn" aria-label="Settings" data-tooltip="Settings">⚙️</button>
  <button class="icon-btn" aria-label="Profile" data-tooltip="Profile">👤</button>
</nav>

<div class="tooltip" id="tooltip" role="tooltip" hidden></div>

```

```css
/* Core tooltip styling per skills/emil-design-eng/SKILL.md */
.tooltip {
  position: fixed;
  padding: 6px 10px;
  background: #18181b;
  color: #fafafa;
  border-radius: 6px;
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  pointer-events: none;
  z-index: 50;
  
  /* Optimized transition: transform + opacity only */
  transition: transform 125ms cubic-bezier(0.25, 0.1, 0.25, 1), 
              opacity 125ms cubic-bezier(0.25, 0.1, 0.25, 1);
  transform-origin: var(--transform-origin);
  
  /* Starting state */
  opacity: 0;
  transform: scale(0.96);
}

.tooltip[hidden] {
  display: none;
}

/* Entered state */
.tooltip[data-visible]:not([data-instant]) {
  opacity: 1;
  transform: scale(1);
}

/* Instant mode: zero‑duration transition */
.tooltip[data-instant] {
  transition-duration: 0ms;
  opacity: 1;
  transform: scale(1);
}

/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
  .tooltip {
    transition-duration: 0ms;
  }
}

```

```javascript
/* Robust tooltip controller */
const tooltip = document.getElementById('tooltip');
let hasShownTooltip = false;
let hideTimeout = null;

document.querySelector('.toolbar').addEventListener('mouseover', (e) => {
  const trigger = e.target.closest('[data-tooltip]');
  if (!trigger) return;

  // Clear any pending hide
  clearTimeout(hideTimeout);

  // Setup
  const content = trigger.getAttribute('data-tooltip');
  tooltip.textContent = content;
  tooltip.removeAttribute('hidden');

  // Position from trigger rect
  const rect = trigger.getBoundingClientRect();
  const centerX = rect.left + rect.width / 2;
  const bottomY = rect.bottom + 6;
  
  tooltip.style.setProperty('--transform-origin', `${centerX}px ${rect.top}px`);
  tooltip.style.left = `${centerX}px`;
  tooltip.style.top = `${bottomY}px`;
  tooltip.style.transform = 'translateX(-50%)';

  // Apply instant mode after first show
  if (hasShownTooltip) {
    tooltip.setAttribute('data-instant', '');
  } else {
    tooltip.removeAttribute('data-instant');
    hasShownTooltip = true;
  }

  // Trigger enter animation (or instant display)
  requestAnimationFrame(() => {
    tooltip.setAttribute('data-visible', '');
  });
});

document.querySelector('.toolbar').addEventListener('mouseout', (e) => {
  const relatedTrigger = e.relatedTarget?.closest('[data-tooltip]');
  if (relatedTrigger) return; // Moving to another tooltip trigger

  tooltip.removeAttribute('data-visible');
  tooltip.removeAttribute('data-instant');
  
  // Delayed cleanup preserves exit animation
  hideTimeout = setTimeout(() => {
    tooltip.setAttribute('hidden', '');
  }, 125);
});

```

## How This Fits the Broader Skills System

The tooltip pattern exemplifies three foundational rules from [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md):

| Rule | Line Reference | Application in Tooltips |
|------|---------------|------------------------|
| **Only animate transform and opacity** | 78–81 | Tooltip uses exactly these two properties, avoiding layout thrash |
| **Use CSS transitions for interruptible UI** | 69–71 | Rapid cursor movement doesn't create "stacked" or stuck animations |
| **Duration under 300ms for UI** | 27–34 | 125ms entrance; 0ms for subsequent hovers |

The pattern also integrates with **accessibility** requirements: the `prefers-reduced-motion` media query (lines 27–33 in the skill's accessibility guidance) ensures users who disable animations still benefit from the instant tooltip behavior without extraneous motion.

## Performance Characteristics

- **Compositing only**: `transform` and `opacity` run on the GPU, eliminating main‑thread layout work
- **No RAF loops**: State changes through CSS attributes, not JavaScript-driven animation frames
- **Memory efficient**: Single shared tooltip element, not one per trigger
- **Interruptible**: Hovering a new button immediately repaints the tooltip; no "finishing" animations queue up

## Summary

- **Initial delay prevents accidents**: Use `data-starting-style` with a JavaScript‑controlled delay before first tooltip display
- **`data-instant` removes all latency**: Set `transition-duration: 0ms` via attribute selector for subsequent hovers
- **CSS‑driven architecture**: Minimize JavaScript; leverage GPU‑accelerated properties for 60fps performance
- **Respect system preferences**: Honor `prefers-reduced-motion` while preserving functional instant behavior
- **Reference implementation**: See [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) lines 47–66 for the canonical specification

## Frequently Asked Questions

### How does the Skills system handle tooltip delays versus instant mode?

The `emil-design-eng` skill in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) specifies an initial delay controlled by JavaScript (typically `setTimeout` removing `data-starting-style`), then toggles `data-instant` after the first tooltip appears. This attribute switches the CSS `transition-duration` to `0ms`, eliminating both delay and animation for all subsequent hovers.

### Why use `data-instant` instead of a CSS class?

Attributes follow the Skills convention of **state‑as‑data**. Classes often carry styling baggage; `data-instant` explicitly signals behavioral state. The selector `.tooltip[data-instant]` has the same specificity as a class but self‑documents that this is a **runtime behavioral flag**, not presentational styling.

### What if the user moves between tooltips very quickly?

The CSS transition architecture handles this inherently. Because the tooltip uses `transform` and `opacity` with CSS transitions (not keyframe animations), the browser **interrupts and replaces** the current transition state immediately. No JavaScript cleanup is required—the tooltip simply repositions and, if `data-instant` is active, snaps to visible without motion.

### Does this pattern work with React or other frameworks?

Yes. The Skills pattern is framework‑agnostic. In React, replace the global `hasShownTooltip` boolean with a `useRef` or component state, and bind `data-instant` via the `data-instant={hasShown ? '' : undefined}` prop pattern. The CSS remains identical, and the performance benefits transfer directly to any virtual‑DOM environment.