This document presents a production-ready tooltip component design focusing on accessibility, dynamic positioning, and performance optimizations for modern web applications.
1. Problem Statement & Requirements
Clarifying questions
- What target browsers/devices should this tooltip support? (e.g., mobile touch, desktop hover, screen readers)
- Should tooltips be accessible via keyboard navigation or only mouse interactions?
- Do we need real-time updates or can tooltips be static once rendered?
- Should tooltips work offline or rely on network connectivity for content?
- What level of accessibility compliance is required? (e.g., WCAG 2.1 AA, full ARIA support)
- Do we need internationalization support for multi-language tooltips?
- Are there existing design system components or UI libraries we should integrate with?
Functional requirements
- Display contextual help text on hover or focus
- Support multiple trigger methods (hover, click, focus)
- Show/hide with appropriate animations or transitions
- Position tooltip relative to trigger element (top, bottom, left, right)
- Handle overflow and boundary detection for positioning
- Support rich content (HTML, markdown, or React elements)
- Allow custom styling and theme integration
- Support for keyboard navigation and screen reader accessibility
Non-functional requirements
- Performance: Tooltip should not cause layout shifts or jank
- Accessibility: Must meet WCAG 2.1 AA standards for keyboard navigation and screen readers
- Scalability: Should handle large numbers of tooltips efficiently
- Maintainability: Component should be easily customizable and extendable
- Bundle size: Should not significantly increase frontend bundle size
Constraints and assumptions
- Component will be used within a React application with TypeScript
- Target modern browsers (Chrome 80+, Firefox 70+, Safari 13+)
- No external dependencies beyond React and standard DOM APIs
- Tooltip content is assumed to be safe HTML or text (no XSS concerns in this scope)
- Positioning logic will use DOM Rect calculations for accuracy
- Component assumes proper CSS styling and theming is provided
Out of scope
- Server-side rendering (SSR) considerations
- Complex animations beyond basic show/hide transitions
- Tooltip content pre-fetching or dynamic loading from external sources
- Multi-language translation integration (i18n)
- Tooltip persistence across page navigation or browser sessions
- Custom tooltip themes beyond basic styling options
2. Component Architecture
graph TD
A[Tooltip] --> B[TooltipTrigger]
A --> C[TooltipContent]
B --> D[TooltipPortal]
C --> E[TooltipArrow]
D --> F[TooltipRoot]
classDef smart fill:#4CAF50,stroke:#333,color:white;
classDef dumb fill:#2196F3,stroke:#333,color:white;
class A,B,C,E,F smart
class D dumb
Component Tree Structure
The Tooltip component is composed of several atomic components:
- TooltipRoot - The root container that manages the tooltip lifecycle and state
- TooltipTrigger - The element that activates the tooltip on hover/focus
- TooltipPortal - Renders the tooltip content outside the DOM tree to avoid z-index issues
- TooltipContent - The actual tooltip content container with positioning logic
- TooltipArrow - A small arrow that points to the trigger element
Component Breakdown
Smart Components
- TooltipRoot: Container that manages state, handles events, and provides context to child components
- TooltipTrigger: Handles hover/focus interactions and manages the tooltip lifecycle
Dumb Components
- TooltipPortal: Renders content in a separate DOM node using React Portal
- TooltipContent: Pure presentation layer with positioning logic
- TooltipArrow: Simple arrow component that renders a triangle
TypeScript Props Contracts
// TooltipTrigger Props
interface TooltipTriggerProps {
children: React.ReactNode;
asChild?: boolean;
disabled?: boolean;
}
// TooltipContent Props
interface TooltipContentProps {
children: React.ReactNode;
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
align?: 'start' | 'center' | 'end';
alignOffset?: number;
avoidCollisions?: boolean;
collisionBoundary?: Element | null;
className?: string;
}
// TooltipRoot Props
interface TooltipRootProps {
children: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
delayMs?: number;
disableHoverableContent?: boolean;
}
// TooltipArrow Props
interface TooltipArrowProps {
className?: string;
width?: number;
height?: number;
}
UI State Machine
stateDiagram-v2
[*] --> Closed
Closed --> Opening : hoverEnter
Opening --> Open : delayMs
Open --> Closing : hoverLeave
Closing --> Closed : delayMs
Open --> Closing : focusOut
Closing --> Closed : focusOut
Closed --> Opening : focusIn
Opening --> Open : delayMs
Open --> Closing : blur
Closing --> Closed : blur
Component Responsibilities
-
TooltipRoot:
- Manages global tooltip state and context
- Handles delay timing for opening/closing
- Provides state to child components via React Context
-
TooltipTrigger:
- Wraps the trigger element that activates the tooltip
- Handles hover/focus events with proper timing delays
- Manages accessibility attributes (aria-describedby)
-
TooltipPortal:
- Renders content outside the normal DOM flow
- Ensures proper z-index handling and positioning
-
TooltipContent:
- Renders the actual tooltip content
- Handles positioning logic based on trigger element
- Manages collision detection and boundary constraints
-
TooltipArrow:
- Renders an arrow pointing to the trigger element
- Handles orientation and positioning
Reusability Decisions
The Tooltip component is designed to be highly reusable across the application:
- Positioning flexibility: Supports all four sides with customizable offsets
- Accessibility-first: Implements proper ARIA attributes and keyboard navigation
- Styling agnostic: Accepts className prop for easy theming
- Context-aware: Uses React Context to maintain state across nested tooltips
Component Lifecycle Considerations
- Mounting: TooltipPortal mounts the content to a separate DOM node
- Updating: Position recalculated on trigger element size changes and window resize
- Unmounting: Cleanup of event listeners and DOM node removal
- State transitions: Delayed open/close behavior to prevent flickering
- Focus management: Proper focus trapping and restoration when tooltips are active
The component architecture follows React best practices with proper separation of concerns, allowing for easy testing and maintenance while providing a flexible API that can be extended for various use cases.
3. State Management Design
Local state vs. global state decisions
For a Tooltip component, local state is sufficient for most use cases. The tooltip's visibility and positioning are inherently tied to the component that triggers it, making global state unnecessary. We'll use React's built-in useState for managing the tooltip's open/closed state and position calculations.
State management approach
We'll use React's built-in state management with useState and useEffect hooks. This approach is optimal because:
- Tooltips are typically lightweight and context-specific
- No complex cross-component state sharing is required
- Reduces dependency on external libraries
- Maintains good performance with minimal re-renders
State shape and normalization strategy
interface TooltipState {
isOpen: boolean;
position: {
top: number;
left: number;
right: number;
bottom: number;
};
placement: 'top' | 'bottom' | 'left' | 'right';
}
Server state vs. client state separation
Tooltips are purely client-side UI components with no server state dependencies. All positioning and visibility logic is handled locally.
Cache invalidation strategy
Since tooltips don't cache data, no cache invalidation strategy is needed.
Optimistic updates design
Tooltips don't require optimistic updates as they're purely UI elements without data mutations.
Critical-path hook implementation
import { useState, useEffect, useRef } from 'react';
interface UseTooltipProps {
placement?: 'top' | 'bottom' | 'left' | 'right';
offset?: number;
delay?: number;
}
interface UseTooltipReturn {
isOpen: boolean;
position: {
top: number;
left: number;
right: number;
bottom: number;
};
showTooltip: () => void;
hideTooltip: () => void;
toggleTooltip: () => void;
}
const useTooltip = ({
placement = 'top',
offset = 8,
delay = 200
}: UseTooltipProps): UseTooltipReturn => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({
top: 0,
left: 0,
right: 0,
bottom: 0
});
const triggerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const calculatePosition = () => {
if (!triggerRef.current || !tooltipRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
let top = 0;
let left = 0;
switch (placement) {
case 'top':
top = triggerRect.top - tooltipRect.height - offset;
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
break;
case 'bottom':
top = triggerRect.bottom + offset;
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
break;
case 'left':
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
left = triggerRect.left - tooltipRect.width - offset;
break;
case 'right':
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
left = triggerRect.right + offset;
break;
}
setPosition({
top,
left,
right: window.innerWidth - (left + tooltipRect.width),
bottom: window.innerHeight - (top + tooltipRect.height)
});
};
const showTooltip = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsOpen(true);
calculatePosition();
}, delay);
};
const hideTooltip = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsOpen(false);
};
const toggleTooltip = () => {
if (isOpen) {
hideTooltip();
} else {
showTooltip();
}
};
useEffect(() => {
if (isOpen) {
calculatePosition();
window.addEventListener('resize', calculatePosition);
window.addEventListener('scroll', calculatePosition);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
window.removeEventListener('resize', calculatePosition);
window.removeEventListener('scroll', calculatePosition);
};
}, [isOpen, placement]);
return {
isOpen,
position,
showTooltip,
hideTooltip,
toggleTooltip
};
};
export default useTooltip;
The hook returns:
isOpen: Boolean indicating tooltip visibilityposition: Object containing calculated positioning valuesshowTooltip,hideTooltip,toggleTooltip: Functions to control tooltip state
Key state transitions:
showTooltip→ setsisOpento true after delay and calculates positionhideTooltip→ setsisOpento false immediatelytoggleTooltip→ toggles between show/hide states
Side effects handled:
- Window resize and scroll event listeners for position recalculations
- Cleanup of timeouts and event listeners to prevent memory leaks
- Delayed execution for hover interactions to avoid flickering
4. Routing & Navigation
Route Structure and Hierarchy
The tooltip component is a presentational UI element that does not require dedicated routing. It's typically rendered as part of other components or pages and doesn't have its own route in the application's URL structure.
// Route structure would look like:
/
/dashboard
/analytics
/reports
/profile
/settings
Code Splitting Strategy
Since tooltips are lightweight UI components that are typically rendered conditionally within existing routes, code splitting is not applicable at the route level. However, tooltip components could be imported dynamically when needed:
// Example of dynamic import for tooltip component (if needed)
const Tooltip = React.lazy(() => import('./components/Tooltip'));
Navigation Guards and Authentication Flows
Tooltips themselves do not participate in navigation guards or authentication flows. They are rendered conditionally based on user interaction and do not affect routing logic.
Deep Linking Support and URL Parameter Design
Tooltips don't require deep linking support or URL parameter handling since they're UI elements that appear on demand. They are typically triggered by hover, click, or focus events rather than URL states.
Browser History Management and Scroll Restoration
Browser history management is not affected by tooltip usage. The component does not modify the browser's history stack or require special scroll restoration handling since it's a transient UI element that appears and disappears based on user interaction.
Component Integration Considerations
The tooltip component should integrate cleanly with existing navigation patterns:
- Positioning: Tooltips must be positioned relative to their trigger elements without interfering with navigation
- Focus Management: When tooltips appear via keyboard focus, proper focus restoration must be maintained
- Accessibility: Tooltips should not break existing keyboard navigation flows or screen reader experiences
// Example integration pattern in a page component:
const MyPage = () => {
return (
<div>
<button
aria-describedby="tooltip-1"
onMouseEnter={() => setShowTooltip(true)}
onFocus={() => setShowTooltip(true)}
>
Hover me
</button>
{showTooltip && (
<Tooltip
id="tooltip-1"
content="This is a tooltip"
position="top"
/>
)}
</div>
);
};
The component architecture ensures that tooltips work seamlessly with existing routing patterns without requiring any special route handling or navigation considerations.
5. API Integration Layer
API client design pattern
For a tooltip component, we don't typically integrate with external APIs since tooltips are usually static content or dynamically generated from local data. However, if the tooltip needs to fetch dynamic content (e.g., user profile info, product details), we would use a centralized API client.
// api/client.ts
import axios, { AxiosInstance } from 'axios';
const createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for auth token injection
client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle auth errors
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return client;
};
export const apiClient = createApiClient();
Request/response interceptors
The API client handles authentication tokens and error responses. For tooltip content, we would typically have:
- Request Interceptor: Adds auth tokens for protected endpoints
- Response Interceptor: Handles 401 errors and normalizes responses
Error handling and retry strategy
For tooltip content, we implement a robust error handling system:
// hooks/useTooltipContent.ts
import { useState, useEffect } from 'react';
import { apiClient } from '../api/client';
interface TooltipContent {
id: string;
title: string;
description: string;
metadata?: Record<string, any>;
}
interface UseTooltipContentResult {
content: TooltipContent | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
const useTooltipContent = (id: string): UseTooltipContentResult => {
const [content, setContent] = useState<TooltipContent | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchContent = async () => {
setLoading(true);
setError(null);
try {
const response = await apiClient.get<TooltipContent>(`/tooltip/${id}`);
setContent(response.data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch tooltip content'));
} finally {
setLoading(false);
}
};
useEffect(() => {
if (id) {
fetchContent();
}
}, [id]);
return {
content,
loading,
error,
refetch: fetchContent,
};
};
export default useTooltipContent;
Loading, error, and empty states design
The tooltip component handles these states gracefully:
// components/Tooltip.tsx
import React, { useState, useEffect } from 'react';
import useTooltipContent from '../hooks/useTooltipContent';
interface TooltipProps {
id: string;
children: React.ReactNode;
delay?: number;
}
const Tooltip: React.FC<TooltipProps> = ({ id, children, delay = 300 }) => {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const { content, loading, error } = useTooltipContent(id);
const handleMouseEnter = () => {
setTimeout(() => setIsVisible(true), delay);
};
const handleMouseLeave = () => {
setIsVisible(false);
};
return (
<div
className="tooltip-trigger"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
{isVisible && (
<div
className="tooltip-content"
style={{ top: position.top, left: position.left }}
>
{loading && <div>Loading...</div>}
{error && <div className="error">Failed to load content</div>}
{content && (
<div>
<h3>{content.title}</h3>
<p>{content.description}</p>
</div>
)}
</div>
)}
</div>
);
};
export default Tooltip;
Data fetching strategy
For static tooltip content, we use:
- CSR (Client-Side Rendering) - For dynamic content that requires user interaction
- SSR (Server-Side Rendering) - For SEO-critical tooltip content that's part of the page structure
API response transformation layer
// api/transformers.ts
import { TooltipContent } from '../types';
export const transformTooltipResponse = (response: any): TooltipContent => {
return {
id: response.id,
title: response.title || '',
description: response.description || '',
metadata: response.metadata || {},
};
};
Sequence diagram for primary data flows
sequenceDiagram
participant User
participant TooltipComponent
participant Hook
participant API
participant Store
User->>TooltipComponent: Hover over tooltip trigger
TooltipComponent->>Hook: Fetch content (with delay)
Hook->>API: GET /tooltip/{id}
API-->>Hook: Response with content
Hook->>TooltipComponent: Update state with content
TooltipComponent->>User: Display tooltip with content
6. Performance Design
Core Web Vitals targets
- Largest Contentful Paint (LCP): <2.5s for tooltip content rendering
- Interaction to Next Paint (INP): <200ms for tooltip show/hide interactions
- Cumulative Layout Shift (CLS): <0.1 for tooltip positioning and content layout
Bundle size budget per route
- Tooltip component: <2KB (minified + gzipped)
- Tooltip dependencies (positioning library): <10KB (minified + gzipped)
- Total tooltip feature bundle: <15KB (including all dependencies)
Critical rendering path optimization
- Tooltip content should be rendered with minimal DOM overhead
- Use CSS transforms for positioning instead of absolute positioning calculations to avoid layout thrashing
- Tooltip content should not block rendering of main content
- Implement CSS containment for tooltip elements to prevent style recalculation cascades
Image optimization strategy
- Tooltip content may include images, so ensure proper lazy loading via
loading="lazy"attribute - Support responsive image formats (webp where available) for any images in tooltip content
- Implement proper
sizesandsrcsetattributes for responsive image delivery
Font loading strategy
- Tooltip content should use system fonts to avoid font loading delays
- Implement font fallbacks for accessibility and performance
- Use
font-display: swapto prevent invisible text during font loading
Memoization strategy
- Memoize: Tooltip content rendering, positioning calculations, and event handlers
- Avoid memoization: Simple state updates (show/hide), DOM ref access, and basic event binding
- Use
useCallbackfor tooltip interaction handlers to prevent unnecessary re-renders - Use
React.memoon tooltip content components when they receive static props
Virtualization for large lists
- Not applicable to tooltip component as it's a single-element UI pattern, not list-based
- However, if tooltips are used in lists (e.g., table cells), ensure proper virtualization of list containers
Prefetching and preloading strategy
- Preload tooltip content when user hovers near relevant elements (e.g., in a dashboard)
- Implement prefetching for tooltips that are likely to be shown soon (e.g., in a navigation menu)
- Use
IntersectionObserverto detect when tooltips might be needed and preload content in advance
// Performance-optimized tooltip component with memoization strategy
import React, { useState, useCallback, useRef, useEffect } from 'react';
const Tooltip = React.memo(({
content,
children,
placement = 'top',
delay = 300
}: {
content: React.ReactNode;
children: React.ReactNode;
placement?: 'top' | 'bottom' | 'left' | 'right';
delay?: number;
}) => {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement>(null);
// Memoize positioning calculation to prevent unnecessary recalculations
const calculatePosition = useCallback((triggerRect: DOMRect, tooltipRect: DOMRect) => {
const positions = {
top: { top: triggerRect.top - tooltipRect.height - 8, left: triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2) },
bottom: { top: triggerRect.bottom + 8, left: triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2) },
left: { top: triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2), left: triggerRect.left - tooltipRect.width - 8 },
right: { top: triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2), left: triggerRect.right + 8 }
};
return positions[placement];
}, [placement]);
// Memoize event handlers to prevent re-creation on every render
const handleMouseEnter = useCallback(() => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
setIsVisible(true);
}, delay);
}, [delay]);
const handleMouseLeave = useCallback(() => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setIsVisible(false);
}, []);
// Use effect to handle positioning updates efficiently
useEffect(() => {
if (isVisible && triggerRef.current && tooltipRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition(calculatePosition(triggerRect, tooltipRect));
}
}, [isVisible, calculatePosition]);
return (
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="tooltip-trigger"
>
{children}
{isVisible && (
<div
ref={tooltipRef}
style={{
position: 'absolute',
top: `${position.top}px`,
left: `${position.left}px`,
transform: 'translateZ(0)', // Hardware acceleration
willChange: 'transform', // Performance hint
zIndex: 1000,
pointerEvents: 'none'
}}
className="tooltip-content"
>
{content}
</div>
)}
</div>
);
});
export default Tooltip;
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent tooltip rendering failures from crashing the entire application. The primary placement is at the root of the tooltip component itself, with secondary boundaries around complex tooltip content that may contain user-provided content or external data.
graph TD
A[App Root] --> B[Tooltip Component]
B --> C[TooltipContent]
C --> D[UserContent/ExternalData]
B --> E[ErrorBoundary]
E --> F[ErrorFallback]
Global Error Handler Design
A global error handler will capture unhandled errors from tooltip components and report them to monitoring systems. The handler will ensure that tooltip errors don't propagate to higher-level error boundaries and provide graceful fallback behavior.
Offline / Network Failure Handling
Tooltips should gracefully handle network failures when fetching dynamic content (e.g., user avatars, external data). The component should:
- Display cached or default content when network is unavailable
- Implement retry logic with exponential backoff for dynamic data
- Show a "Network unavailable" indicator when appropriate
Graceful Degradation Patterns
When tooltips fail to render properly, the system should degrade gracefully:
- Fallback to simple text-only tooltips
- Disable animations or complex rendering
- Show minimal content with clear error indicators
- Maintain keyboard navigation accessibility even when tooltip fails
User Feedback on Errors
Tooltips should provide clear feedback when they fail to render:
- Inline error messages for content errors
- Toast notifications for critical tooltip failures
- Fallback UI that maintains the component's purpose
- Accessible error states with proper ARIA attributes
// Error boundary implementation for tooltip component
interface TooltipErrorState {
hasError: boolean;
error?: Error;
}
class TooltipErrorBoundary extends React.Component<{}, TooltipErrorState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): TooltipErrorState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to monitoring service
console.error('Tooltip component error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="tooltip-fallback" role="alert">
<span className="tooltip-error-icon">⚠️</span>
<span>Tooltip content unavailable</span>
</div>
);
}
return this.props.children;
}
}
Error Recovery Strategy
Tooltips should implement automatic recovery when transient errors occur:
- Retry failed content fetches after a delay
- Cache successful tooltip states for quick recovery
- Implement circuit breaker pattern for persistent errors
- Provide user-initiated refresh capability for failed tooltips
Fallback UI Design
When tooltip rendering fails, the fallback should:
- Preserve the tooltip's positioning context
- Show minimal content that conveys the same information
- Maintain accessibility attributes and keyboard navigation
- Provide clear indication that content is not fully loaded
// Fallback tooltip implementation
const TooltipFallback = ({ message }: { message?: string }) => (
<div
className="tooltip tooltip-fallback"
role="tooltip"
aria-live="polite"
>
<div className="tooltip-content">
{message || "Tooltip content unavailable"}
</div>
</div>
);
Monitoring Integration
All tooltip errors should be logged with:
- Error type and stack trace
- User context (if available)
- Component state at time of error
- Browser and environment information
- Retry count and recovery status
Retry Logic Implementation
For network-dependent tooltip content:
const useTooltipRetry = (maxRetries = 3) => {
const [retryCount, setRetryCount] = useState(0);
const retry = useCallback(() => {
if (retryCount < maxRetries) {
setRetryCount(prev => prev + 1);
// Implement exponential backoff
setTimeout(() => {
// Retry logic here
}, Math.pow(2, retryCount) * 1000);
}
}, [retryCount, maxRetries]);
return { retryCount, retry };
};
Performance Impact Considerations
Error handling should not impact tooltip performance:
- Error boundaries should be lightweight
- Fallback rendering should not block main thread
- Error logging should be asynchronous when possible
- Memory cleanup for failed tooltip instances
8. Testing Strategy
Unit Testing Approach
Component Tests (70% of test coverage)
- Test all tooltip variants:
top,bottom,left,rightwith proper positioning - Validate content rendering with text, JSX elements, and React components
- Test
delay,disabled,visibleprops behavior - Verify proper cleanup of event listeners and timers on unmount
- Test
onOpenandonClosecallback execution with proper timing
Utility Tests (20% of test coverage)
- Test positioning calculation logic with various element sizes and viewport dimensions
- Validate
getTooltipPositionfunction with edge case handling (viewport overflow, element centering) - Test
useTooltipcustom hook state transitions and side effects
Mocked Integration Tests (10% of test coverage)
- Test tooltip behavior in complex layouts with scrollable containers
- Validate proper positioning when parent elements have
transformorposition: fixed
Integration Testing Strategy
Cross-browser compatibility testing
- Test tooltip positioning and rendering in Chrome, Firefox, Safari, Edge
- Validate proper behavior with different browser zoom levels (100%, 125%, 150%)
Layout boundary testing
- Test tooltip positioning when element is near viewport edges
- Validate proper alignment when parent container has
overflow: hidden - Test tooltip behavior inside scrollable containers with
position: fixed
E2E Testing Scope
Cypress-based E2E coverage (50% of E2E tests)
- User interaction flows: hover → tooltip appears → mouse moves to tooltip → tooltip stays open
- Keyboard navigation support (Tab, Enter, Space) for accessible tooltips
- Multi-tooltip scenarios: multiple tooltips on same page with proper isolation
- Responsive behavior testing across mobile, tablet, desktop breakpoints
Critical user journeys:
- Hover over element with tooltip → verify tooltip appears within 200ms
- Move mouse to tooltip content → ensure tooltip remains open
- Move mouse away from element and tooltip → verify tooltip closes within 200ms
- Keyboard focus navigation through elements with tooltips
- Tooltip content updates when underlying data changes
Visual Regression Testing
Screenshot testing with Percy/Cypress
- Capture tooltip positioning in various layouts (top, bottom, left, right)
- Test with different content types (text only, rich text, React components)
- Validate proper rendering in dark/light mode variants
- Test responsive behavior across breakpoints (mobile, tablet, desktop)
Accessibility Testing Automation
axe-core integration with Jest
- Test WCAG 2.1 AA compliance for all tooltip variants
- Validate proper ARIA attribute usage (
aria-describedby,role="tooltip") - Verify keyboard navigation accessibility
- Test screen reader compatibility with NVDA, JAWS, VoiceOver
Test Data Strategy
Test fixtures and mock data
- Create minimal test data for basic tooltip content (text, JSX)
- Generate edge case scenarios: very long text, empty content, complex React components
- Mock DOM element sizes and positions to simulate various viewport conditions
- Use deterministic test IDs for reliable E2E selectors
Test coverage targets:
- Component unit tests: 90% code coverage (line + branch)
- Integration tests: 85% of key user flows covered
- E2E tests: 70% of critical user journeys tested end-to-end
- Accessibility tests: 100% WCAG AA compliance verified
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The tooltip component must meet the following WCAG 2.1 AA success criteria:
- 1.1.1 Non-text Content: All tooltip content must be text-based or have appropriate text alternatives for non-text content
- 1.4.13 Content on Hover or Focus: Tooltip content must be accessible when the trigger element receives focus (keyboard navigation) and not rely solely on hover
- 1.4.3 Contrast Ratio: Tooltip text must have a contrast ratio of at least 4.5:1 against the background
- 2.1.1 Keyboard: All tooltip functionality must be operable via keyboard navigation alone
- 2.4.6 Headings and Labels: Tooltip trigger elements must have appropriate labels for screen readers
- 3.1.1 Language: Tooltip content should respect language attributes when displaying translated text
Keyboard navigation support
The tooltip component must support full keyboard navigation:
- Tab Navigation: Users can navigate to the trigger element using Tab key
- Enter/Space Activation: Pressing Enter or Space while focused on the trigger activates the tooltip
- Escape Dismissal: Pressing Escape key dismisses the active tooltip
- Focus Management: When tooltip is shown, focus should not move away from the trigger element unless explicitly designed to do so
Focus management strategy
The tooltip component implements proper focus management:
- Focus Trapping: When the tooltip is open, keyboard focus remains on the trigger element
- Focus Restoration: When tooltip closes (via Escape or click outside), focus returns to the trigger element
- Focusable Elements: Tooltip content should not contain focusable elements unless explicitly designed to be interactive
ARIA roles and attributes plan
The tooltip component uses appropriate ARIA attributes:
// Tooltip trigger element
<button
aria-describedby="tooltip-id"
aria-expanded="false" // updated when tooltip opens
aria-label="Tooltip description"
>
{triggerContent}
</button>
// Tooltip element
<div
id="tooltip-id"
role="tooltip"
aria-hidden="true" // updated when tooltip is visible
className="tooltip-content"
>
{tooltipContent}
</div>
Screen reader compatibility
The tooltip component ensures screen reader compatibility:
- Live Region: Tooltip content is announced when it becomes visible
- Descriptive Labels: Trigger elements have descriptive labels that provide context for the tooltip content
- Announcement Timing: Screen readers announce tooltip content immediately upon activation, not after a delay
- No Conflicting Announcements: Tooltip content does not interfere with existing screen reader announcements
Additional accessibility considerations
- Announcement Delay: Tooltip content should be announced immediately upon activation (no artificial delays)
- Interactive Content: If tooltip contains interactive elements, they must be properly labeled and keyboard accessible
- Dynamic Content Updates: When tooltip content changes dynamically, screen readers must be notified of the change
- Color Contrast: Tooltip background and text must meet color contrast requirements for accessibility
- Focus Indicators: The tooltip trigger element should maintain visible focus indicators when focused
Keyboard interaction flow
stateDiagram-v2
[*] --> Idle
Idle --> Showing : Focus + Enter/Space
Showing --> Hiding : Escape
Showing --> Idle : Click outside
Hiding --> Idle : Animation complete
The tooltip component ensures that all keyboard interactions are fully accessible and meet WCAG 2.1 AA standards, providing equivalent functionality for users who rely on keyboard navigation or screen readers.
10. Security Considerations
XSS Prevention Strategy
The Tooltip component must prevent Cross-Site Scripting (XSS) attacks by sanitizing all content passed to it. Since tooltips can display user-generated content or dynamic text, raw HTML injection is a potential vulnerability.
// Secure tooltip content handling
const sanitizeTooltipContent = (content: string): string => {
const div = document.createElement('div');
div.textContent = content;
return div.innerHTML;
};
// Alternative using DOMPurify for complex HTML
import DOMPurify from 'dompurify';
const sanitizeTooltipWithDOMPurify = (content: string): string => {
return DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: [],
});
};
CSRF Protection
While tooltips themselves don't typically make API calls, if they're used in contexts where dynamic content is fetched (e.g., tooltips that load from a backend), CSRF tokens should be handled at the application level through proper API client configuration.
Content Security Policy (CSP) Headers
The tooltip component should not introduce CSP violations. All inline styles and scripts must be avoided:
// ❌ Avoid - This would violate CSP
const tooltipStyle = `position: absolute; left: 0; top: 0;`;
// ✅ Safe - Use CSS classes instead
const tooltipStyle = 'tooltip-positioned';
Sensitive Data Handling in Frontend State
Tooltips should never expose sensitive data through their content or props. The component must validate that:
- No authentication tokens, session IDs, or PII are passed as tooltip content
- Tooltip content is filtered to exclude sensitive fields from dynamic data sources
// Example validation for tooltip content
const validateTooltipContent = (content: any): boolean => {
if (typeof content === 'string') return true;
if (typeof content === 'object' && content !== null) {
// Check for sensitive keys in object
const sensitiveKeys = ['token', 'sessionId', 'password', 'secret'];
return !Object.keys(content).some(key =>
sensitiveKeys.includes(key.toLowerCase())
);
}
return false;
};
Input Sanitization Approach
All tooltip content should be sanitized before rendering:
// Sanitize tooltip content in component
const Tooltip: React.FC<TooltipProps> = ({
content,
children,
...rest
}) => {
const sanitizedContent = useMemo(() => {
if (typeof content === 'string') {
return sanitizeTooltipContent(content);
}
// For complex content, use DOMPurify or similar
return sanitizeTooltipWithDOMPurify(JSON.stringify(content));
}, [content]);
return (
<div
{...rest}
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
>
{children}
</div>
);
};
Security Headers Considerations
The tooltip component should not require any special security headers but must ensure that any dynamically generated content is properly escaped to prevent XSS in environments with strict CSP policies.
Risk Mitigation
- Content Validation: All tooltip content must be validated and sanitized before rendering
- Secure Rendering: Never use
dangerouslySetInnerHTMLwithout proper sanitization - Input Filtering: Implement strict input filtering for dynamic tooltip content
- CSP Compliance: Ensure no inline styles or scripts are generated by the component
- Audit Trail: Log any tooltip content that might contain sensitive data for security review
The component should be designed with security as a primary concern, treating all user-provided or dynamic content as potentially malicious and applying appropriate sanitization techniques.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys for tooltips will follow a consistent naming pattern to ensure maintainability and scalability:
tooltip.{componentName}.{property}
For example:
tooltip.button.savetooltip.input.emailtooltip.card.userProfile
Keys will be organized in nested JSON files per locale:
src/locales/en/tooltip.json
src/locales/fr/tooltip.json
src/locales/es/tooltip.json
RTL Support Considerations
RTL support will be handled through CSS direction properties and layout adjustments:
- Tooltip container will use
direction: rtlwhen locale is RTL - Arrow positioning will be inverted for RTL layouts (left arrow becomes right)
- Text alignment will be set to
rightin RTL contexts - Popover positioning logic will account for RTL by adjusting horizontal offsets
Date, Number, Currency Formatting Strategy
Tooltips that display dynamic content will use the Intl API for proper formatting:
// Example of date formatting in tooltip content
const formatDate = (date: Date, locale: string) => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
// Example of number formatting in tooltip content
const formatNumber = (value: number, locale: string) => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(value);
};
Locale Detection and Switching Strategy
-
Automatic Detection:
- Use
navigator.languageas primary source - Fallback to
en-USif no supported language detected - Store preferred locale in localStorage for persistence
- Use
-
Manual Switching:
- Provide a language selector component in the UI
- Update
i18nextinstance with new locale - Trigger re-render of all translated components
-
Server-side Considerations:
- Pass locale in HTTP headers or query parameters for SSR
- Use
Accept-Languageheader for automatic detection
// Example locale switching hook
const useLocaleSwitcher = () => {
const [currentLocale, setCurrentLocale] = useState<string>('en-US');
const switchLocale = useCallback((newLocale: string) => {
i18next.changeLanguage(newLocale);
setCurrentLocale(newLocale);
localStorage.setItem('preferredLocale', newLocale);
}, []);
useEffect(() => {
const savedLocale = localStorage.getItem('preferredLocale');
if (savedLocale && i18next.language !== savedLocale) {
i18next.changeLanguage(savedLocale);
setCurrentLocale(savedLocale);
}
}, []);
return { currentLocale, switchLocale };
};
12. Monitoring & Observability
Real User Monitoring (RUM) metrics to track
The tooltip component should capture key performance indicators for user experience monitoring:
- Interaction latency: Time from user hover/focus to tooltip appearance (target < 100ms)
- Tooltip visibility duration: How long tooltips remain visible to users (target > 200ms for readability)
- Tooltip activation rate: Percentage of tooltip triggers that result in visible tooltips (target > 95% success rate)
- Scroll performance impact: Tooltip behavior during scroll operations (max 100ms additional render time)
- Memory usage: Tooltip instances and DOM nodes created per user session (target < 50ms heap increase)
Error tracking integration
Integrate with Sentry or similar error monitoring tools to capture:
- Tooltip rendering failures (e.g., DOM element not found, positioning errors)
- State transition errors in tooltip lifecycle management
- Accessibility violation reports (focus trap issues, ARIA attribute problems)
- Performance degradation alerts when tooltip interactions exceed thresholds
Custom events and user journey tracking
Implement custom event tracking for:
// Event tracking for tooltip interactions
const trackTooltipEvent = (event: 'tooltip_open' | 'tooltip_close' | 'tooltip_hover' | 'tooltip_focus') => {
// Send to analytics service (e.g., Mixpanel, Google Analytics)
if (window.mixpanel) {
window.mixpanel.track(`tooltip_${event}`, {
timestamp: Date.now(),
tooltipId: 'unique-tooltip-id',
userAction: event,
viewportPosition: { x: window.scrollX, y: window.scrollY }
});
}
};
Performance monitoring and regression detection
- LCP (Largest Contentful Paint): Ensure tooltip content doesn't block main thread rendering
- FID (First Input Delay): Monitor that tooltip interactions don't increase input delay beyond 100ms
- CLS (Cumulative Layout Shift): Track that tooltip positioning doesn't cause layout shifts > 0.1
- Bundle size impact: Monitor tooltip component bundle size to ensure it stays under 2KB (minified + gzipped)
- Memory leak detection: Monitor tooltip instance count over time to detect potential memory leaks
Performance monitoring will be implemented using:
- Web Vitals SDK for real-time performance metrics
- Custom performance observer to track tooltip-specific rendering behavior
- Automated regression testing in CI pipeline to catch performance degradation
Observability tooling integration
The tooltip component will integrate with:
- Sentry Performance Monitoring: For error tracking and performance insights
- DataDog APM: For end-to-end transaction tracing including tooltip interactions
- Lighthouse CI: For automated accessibility and performance audits
- Chrome DevTools Profiler: For detailed rendering and memory usage analysis
The component will include optional telemetry configuration that allows teams to:
- Enable/disable specific monitoring points
- Set custom thresholds for performance alerts
- Configure sampling rates for high-volume tooltip interactions
- Send anonymized usage analytics to product teams for feature adoption insights
// Example telemetry configuration interface
interface TooltipTelemetryConfig {
enabled: boolean;
performanceMetrics: {
thresholdMs: number;
sampleRate: number;
};
errorReporting: {
enabled: boolean;
reportToSentry?: boolean;
};
userJourneyTracking: {
enabled: boolean;
events: string[];
};
}
13. File & Folder Structure
Proposed Directory Structure
src/
├── components/
│ ├── Tooltip/
│ │ ├── index.ts
│ │ ├── Tooltip.tsx
│ │ ├── Tooltip.types.ts
│ │ ├── Tooltip.module.css
│ │ └── __tests__/
│ │ └── Tooltip.test.tsx
├── features/
│ └── tooltip/
│ ├── index.ts
│ └── hooks/
│ └── useTooltip.ts
└── types/
└── tooltip.ts
Module Boundaries and Import Rules
- Tooltip component is self-contained with no cross-feature dependencies
- All internal imports use relative paths (
../) - Shared utilities and types are imported from
src/types/orsrc/utils/ - No circular dependencies between feature modules
Shared vs Feature-Specific Code Separation
Shared code:
- Common types (
TooltipPosition,TooltipTrigger) insrc/types/tooltip.ts - CSS modules for consistent styling
- Reusable utility functions (e.g.,
getBoundingClientRect)
Feature-specific code:
- Component implementation and tests
- Custom hooks for tooltip behavior
- Feature-level exports and barrel files
This structure follows a feature-based organization that scales well for teams working on multiple UI components, with clear separation between shared and feature-specific concerns. The directory structure makes it easy to locate and test the tooltip component in isolation while maintaining clean import paths.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React (v18+)
- Justification: Core library for building UI components with hooks, context, and proper component lifecycle management. Essential for any modern React application.
- Bundle Impact: ~100KB (minified + gzip)
- Alternatives Considered: Preact - smaller bundle but less ecosystem support and more complex migration path
2. TypeScript (v5+)
- Justification: Provides type safety that prevents runtime errors and improves developer experience with better IDE support. Critical for maintaining large-scale frontend applications.
- Bundle Impact: None (compile-time only)
- Alternatives Considered: Flow - more complex setup, less adoption in the React ecosystem
3. Styled Components / Tailwind CSS
- Justification: For styling, Tailwind CSS is chosen for its utility-first approach that enables rapid development and consistent design system implementation. It provides good performance with minimal runtime overhead.
- Bundle Impact: ~10KB (Tailwind) vs ~25KB (Styled Components)
- Alternatives Considered: CSS Modules - more verbose, less maintainable for large teams
4. Framer Motion (v10+)
- Justification: Provides smooth animations and transitions for tooltip show/hide states. Enables declarative animation control with minimal code overhead.
- Bundle Impact: ~20KB (minified + gzip)
- Alternatives Considered: CSS transitions - more verbose, less flexible for complex animations
5. React Portal (built-in)
- Justification: Required to render tooltips outside of their parent DOM context to prevent z-index and overflow issues.
- Bundle Impact: None (built-in)
- Alternatives Considered: Manual DOM manipulation - more error-prone and harder to maintain
6. @floating-ui/react (v0.13+)
- Justification: Provides robust positioning logic for tooltips that handles edge cases like viewport boundaries, collision detection, and responsive positioning.
- Bundle Impact: ~15KB (minified + gzip)
- Alternatives Considered: Popper.js - more complex API, less active maintenance
Alternatives Considered and Rejected
1. React Tooltip (v4+)
- Rejected: While feature-rich, it's overly heavy (~50KB) and introduces unnecessary complexity for a simple tooltip component. The design system integration would require extensive customization.
2. React Bootstrap Tooltip
- Rejected: Tied to Bootstrap's design system and CSS framework, which would force unwanted styling dependencies. Not suitable for custom design systems.
3. CSS-only tooltip libraries
- Rejected: Lack of proper positioning logic and accessibility features, requiring extensive custom JavaScript to handle edge cases.
Licensing and Bundle Size Impact Assessment
Bundle Size Budget:
- Core dependencies (React + TypeScript) ~120KB
- Styling and animation libraries ~35KB
- Positioning library ~15KB
- Total estimated runtime bundle size: ~170KB
License Compliance: All selected libraries are MIT licensed with no restrictions on commercial usage. No proprietary dependencies that would require enterprise licensing.
Tree-shaking Strategy: All libraries support tree-shaking for optimal bundle size. Only required components are imported to minimize final bundle size.
Performance Impact: The chosen libraries provide minimal runtime overhead while offering essential functionality. Framer Motion and Floating UI are optimized for performance with proper memoization and minimal DOM operations.
Maintenance Considerations: All libraries are actively maintained with regular updates. Floating UI and Framer Motion have strong community support and clear migration paths for version upgrades.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Tooltip placement strategy: Should tooltips always appear above/below elements or support dynamic positioning? This affects the complexity of the positioning engine and whether we should use a library like Popper.js vs. custom implementation.
- Trigger behavior: Should tooltips activate on hover, click, or both? This impacts the component's API surface and accessibility requirements.
- Animation timing: What are the specific animation durations and easing functions? These affect perceived performance and user experience consistency.
- Content composition: Should tooltips support rich content (HTML, React components) or be limited to plain text? This affects the component's flexibility and security considerations.
Known trade-offs made in this design
- Performance vs. Feature Richness: Prioritized lightweight implementation over complex features like auto-scroll adjustment or dynamic content rendering to maintain optimal bundle size and rendering performance.
- Accessibility vs. Simplicity: Chose a minimal ARIA implementation to reduce complexity, accepting that more advanced keyboard navigation patterns might be lost in favor of basic screen reader support.
- Custom vs. Third-party Implementation: Decided against using a mature tooltip library like react-tooltip to maintain full control over the component's behavior and avoid dependency bloat, but this increases implementation complexity.
- Static vs. Dynamic Positioning: Used a simpler static positioning model rather than complex adaptive positioning to reduce code surface area and potential edge case bugs.
Risks and mitigations
- Positioning overflow: Risk that tooltips may render outside viewport boundaries. Mitigation: Implement boundary detection and automatic repositioning logic.
- Accessibility regressions: Risk of breaking keyboard navigation or screen reader support. Mitigation: Comprehensive accessibility testing and integration with existing A11y testing frameworks.
- Performance impact: Risk of layout thrashing or excessive re-renders in high-frequency usage. Mitigation: Thorough performance testing with React DevTools Profiler and implementing proper memoization.
- Cross-browser compatibility: Risk of inconsistent rendering across browsers. Mitigation: Comprehensive browser testing and graceful degradation strategies for older browsers.
Technical debt acknowledged
- Limited content flexibility: The current design restricts tooltip content to simple text, which may require refactoring if rich content support is needed in the future.
- Basic positioning logic: The current placement algorithm only handles top/bottom/left/right positions, requiring future enhancement for more complex positioning strategies.
- No built-in theming support: The component uses hardcoded styles that may not integrate well with design systems, requiring future abstraction for theme customization.
- Limited animation control: Animation timing is hardcoded, which may not accommodate all design system requirements without refactoring.