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

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

/* 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

/* 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:

/* 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 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>
/* 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;
  }
}
/* 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:

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →