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:
- First hover: Delayed appearance (typically 150–300 ms) to confirm intentional interaction
- 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:
transformandopacityrun 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-stylewith a JavaScript‑controlled delay before first tooltip display data-instantremoves all latency: Settransition-duration: 0msvia attribute selector for subsequent hovers- CSS‑driven architecture: Minimize JavaScript; leverage GPU‑accelerated properties for 60fps performance
- Respect system preferences: Honor
prefers-reduced-motionwhile preserving functional instant behavior - Reference implementation: See
skills/emil-design-eng/SKILL.mdlines 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →