This document presents a production-ready low-level design for a reusable Button component that balances flexibility with type safety, accessibility compliance, and theme integration across modern frontend applications.
1. Problem Statement & Requirements
Clarifying questions
- What target browsers/devices should this component support? (e.g., IE11, mobile-first, touch devices)
- Do we need to support internationalization (i18n) for button text and accessibility labels?
- Should this component support real-time state changes (e.g., loading states, disabled states) or is it primarily static?
- Is offline support required for button interactions (e.g., optimistic UI patterns)?
- What level of accessibility compliance is needed? (WCAG 2.1 AA, AAA, or just basic keyboard support?)
- Are there existing design system components that this should integrate with?
- Should the button support custom styling overrides or be strictly controlled by design tokens?
Functional requirements
- Button must support multiple visual variants (primary, secondary, tertiary, outline, ghost)
- Button must support different sizes (small, medium, large)
- Button must support icon placement (left, right, or only icon)
- Button must handle click and keyboard events
- Button must support loading states with spinner animation
- Button must support disabled state with appropriate visual feedback
- Button must support various content types (text, icon, or combination)
- Button must support custom styling overrides
Non-functional requirements
- Performance: Button should render efficiently with minimal DOM overhead
- Accessibility: Full keyboard navigation support and ARIA attributes
- Scalability: Component should be easily extensible for new variants
- Maintainability: Clear component structure with well-defined props contracts
- Consistency: Follow existing design system color/spacing guidelines
Constraints and assumptions
- Component will be built using React with TypeScript
- Will assume a design system with defined color/size tokens
- Component will not include built-in routing or navigation logic
- Will assume standard DOM event handling (no custom event system)
- Component will not handle form submission logic directly
Out of scope
- Form integration (submit, reset, etc.)
- Complex state management (e.g., multi-step button flows)
- Server-side rendering considerations beyond basic component rendering
- Custom animation libraries (will use CSS transitions only)
- Advanced accessibility features beyond standard ARIA roles
- Button group or toolbar component (separate concern)
- Internationalization of button text content (assumes parent handles translation)
2. Component Architecture
graph TD
A[Button] --> B[ButtonIcon]
A --> C[ButtonSpinner]
A --> D[ButtonLabel]
A --> E[ButtonWrapper]
E --> F[ButtonBase]
F --> G[ButtonCore]
F --> H[ButtonVariant]
F --> I[ButtonSize]
F --> J[ButtonState]
Component Tree Analysis
The Button component is designed as a composition of multiple granular components:
- Button (Smart Container): Main component that orchestrates all child components and handles state transitions
- ButtonBase (Smart Container): Handles core DOM attributes, event handling, and accessibility attributes
- ButtonCore (Dumb Presentational): Renders the base HTML button element with proper attributes
- ButtonVariant (Dumb Presentational): Applies variant-specific styling and classes
- ButtonSize (Dumb Presentational): Handles size-specific CSS classes
- ButtonState (Dumb Presentational): Manages state-dependent styling and behavior
- ButtonIcon (Dumb Presentational): Renders optional icons with proper spacing and alignment
- ButtonSpinner (Dumb Presentational): Displays loading spinner during async operations
- ButtonLabel (Dumb Presentational): Renders the button text content
TypeScript Props Contracts
// Button component props
interface ButtonProps {
/**
* Button variant - primary, secondary, outline, ghost, link
*/
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
/**
* Button size - small, medium, large
*/
size?: 'small' | 'medium' | 'large';
/**
* Button state - default, loading, disabled
*/
state?: 'default' | 'loading' | 'disabled';
/**
* Button icon - optional icon component or string
*/
icon?: React.ReactNode | string;
/**
* Icon position - left or right
*/
iconPosition?: 'left' | 'right';
/**
* Button text content
*/
children: React.ReactNode;
/**
* Click handler function
*/
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/**
* Button type - button, submit, reset
*/
type?: 'button' | 'submit' | 'reset';
/**
* Button disabled state
*/
disabled?: boolean;
/**
* Button aria-label for accessibility
*/
ariaLabel?: string;
/**
* Button aria-describedby for accessibility
*/
ariaDescribedby?: string;
/**
* Button id attribute
*/
id?: string;
/**
* Button className for custom styling
*/
className?: string;
}
// ButtonBase component props (internal)
interface ButtonBaseProps {
variant: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
size: 'small' | 'medium' | 'large';
state: 'default' | 'loading' | 'disabled';
type: 'button' | 'submit' | 'reset';
disabled: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
ariaLabel?: string;
ariaDescribedby?: string;
id?: string;
className?: string;
children: React.ReactNode;
}
// ButtonState component props (internal)
interface ButtonStateProps {
state: 'default' | 'loading' | 'disabled';
loadingText?: string;
children: React.ReactNode;
}
UI State Machine
stateDiagram-v2
[*] --> Default
Default --> Loading : onClick
Default --> Disabled : disabled=true
Loading --> Default : onClick
Loading --> Disabled : disabled=true
Disabled --> Default : disabled=false
Disabled --> Loading : onClick
The Button component has three primary states:
- Default: Normal state with interactive behavior
- Loading: Shows spinner and disables interaction during async operations
- Disabled: Completely inactive state with visual indication
State transitions occur based on:
- User interaction (onClick)
- Programmatic updates (disabled prop changes)
- Loading state management
Component Responsibilities and Props Contract
Button Component:
- Orchestrates all child components
- Manages overall state transitions
- Handles event propagation and accessibility
- Accepts all user-facing props
ButtonBase Component:
- Renders the actual HTML button element
- Handles DOM attributes and event handlers
- Manages accessibility attributes (aria-label, aria-describedby)
- Passes down variant and size props to child components
ButtonVariant Component:
- Applies variant-specific CSS classes (primary, secondary, etc.)
- Handles visual styling differences between variants
- Manages variant-specific hover/focus states
ButtonSize Component:
- Applies size-specific CSS classes (small, medium, large)
- Handles sizing constraints and spacing
- Manages responsive behavior
ButtonState Component:
- Manages state-dependent rendering logic
- Handles loading spinner display
- Controls disabled state behavior and styling
Reusability Decisions
The Button component is designed for maximum reusability:
- Variants: Support 5 distinct visual variants (primary, secondary, outline, ghost, link)
- Sizes: Support 3 size options (small, medium, large) for responsive design
- Icon support: Flexible icon placement (left/right) with proper spacing
- Accessibility: Full ARIA compliance and keyboard navigation support
- Extensible design: Easy to add new variants/sizes without breaking changes
Component Lifecycle Considerations
- Mount: Initialize accessibility attributes, set up event listeners
- Update: Handle prop changes (disabled, loading state, variant)
- Unmount: Clean up any event listeners or subscriptions
- State transitions: Handle loading state changes with proper visual feedback
- Focus management: Ensure proper focus behavior for keyboard users
The component uses React's built-in lifecycle methods with proper cleanup in useEffect hooks to prevent memory leaks and ensure clean state management.
3. State Management Design
Local state vs. global state decisions
For a reusable Button component, the state management is minimal and primarily local. The component needs to manage:
- Loading state (for async operations)
- Hover/focus states for accessibility
- Active/pressed states for interactive feedback
- Disabled state management
These are all component-local concerns that don't require global state management.
State management approach
React Context API with useReducer - This is the optimal choice for this component because:
- It provides a clean, predictable state management pattern
- It's built into React and doesn't add external dependencies
- It supports complex state transitions with minimal boilerplate
- It enables easy testing and debugging
- It's scalable - if the component grows in complexity, it can be refactored without major architectural changes
State shape and normalization strategy
interface ButtonState {
isLoading: boolean;
isDisabled: boolean;
isFocused: boolean;
isHovered: boolean;
isActive: boolean;
}
Server state vs. client state separation
For a basic Button component, there's no server state involved. The component only manages its own client-side interactive states.
Cache invalidation strategy
Not applicable for a basic Button component.
Optimistic updates design
Not applicable for a basic Button component.
Critical-path hook implementation
import { useState, useReducer, useCallback, useEffect } from 'react';
// Action types for button state transitions
type ButtonAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_DISABLED'; payload: boolean }
| { type: 'SET_FOCUSED'; payload: boolean }
| { type: 'SET_HOVERED'; payload: boolean }
| { type: 'SET_ACTIVE'; payload: boolean };
// Button state reducer
const buttonReducer = (state: ButtonState, action: ButtonAction): ButtonState => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_DISABLED':
return { ...state, isDisabled: action.payload };
case 'SET_FOCUSED':
return { ...state, isFocused: action.payload };
case 'SET_HOVERED':
return { ...state, isHovered: action.payload };
case 'SET_ACTIVE':
return { ...state, isActive: action.payload };
default:
return state;
}
};
// Button props interface
interface ButtonProps {
children: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
disabled?: boolean;
loading?: boolean;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'small' | 'medium' | 'large';
type?: 'button' | 'submit' | 'reset';
className?: string;
ariaLabel?: string;
ariaExpanded?: boolean;
ariaControls?: string;
}
// Button state interface
interface ButtonState {
isLoading: boolean;
isDisabled: boolean;
isFocused: boolean;
isHovered: boolean;
isActive: boolean;
}
// Custom hook for button state management
export const useButtonState = (props: ButtonProps) => {
const [state, dispatch] = useReducer(buttonReducer, {
isLoading: props.loading || false,
isDisabled: props.disabled || false,
isFocused: false,
isHovered: false,
isActive: false,
});
// Handle loading state changes
useEffect(() => {
if (props.loading !== undefined) {
dispatch({ type: 'SET_LOADING', payload: props.loading });
}
}, [props.loading]);
// Handle disabled state changes
useEffect(() => {
if (props.disabled !== undefined) {
dispatch({ type: 'SET_DISABLED', payload: props.disabled });
}
}, [props.disabled]);
// Event handlers
const handleMouseEnter = useCallback(() => {
dispatch({ type: 'SET_HOVERED', payload: true });
}, []);
const handleMouseLeave = useCallback(() => {
dispatch({ type: 'SET_HOVERED', payload: false });
}, []);
const handleFocus = useCallback(() => {
dispatch({ type: 'SET_FOCUSED', payload: true });
}, []);
const handleBlur = useCallback(() => {
dispatch({ type: 'SET_FOCUSED', payload: false });
}, []);
const handleMouseDown = useCallback(() => {
dispatch({ type: 'SET_ACTIVE', payload: true });
}, []);
const handleMouseUp = useCallback(() => {
dispatch({ type: 'SET_ACTIVE', payload: false });
}, []);
// Combined click handler with async support
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (state.isDisabled || state.isLoading) return;
const result = props.onClick?.(event);
// Handle async operations
if (result instanceof Promise) {
dispatch({ type: 'SET_LOADING', payload: true });
result.finally(() => {
dispatch({ type: 'SET_LOADING', payload: false });
});
}
},
[state.isDisabled, state.isLoading, props.onClick]
);
// Return state and handlers
return {
...state,
handleMouseEnter,
handleMouseLeave,
handleFocus,
handleBlur,
handleMouseDown,
handleMouseUp,
handleClick,
};
};
// Button component using the hook
const Button: React.FC<ButtonProps> = (props) => {
const {
children,
onClick,
disabled,
loading,
variant = 'primary',
size = 'medium',
type = 'button',
className = '',
ariaLabel,
ariaExpanded,
ariaControls,
} = props;
const {
isLoading,
isDisabled,
isFocused,
isHovered,
isActive,
handleMouseEnter,
handleMouseLeave,
handleFocus,
handleBlur,
handleMouseDown,
handleMouseUp,
handleClick,
} = useButtonState(props);
const buttonClasses = [
`btn btn-${variant} btn-${size}`,
isLoading ? 'btn-loading' : '',
isDisabled ? 'btn-disabled' : '',
isFocused ? 'btn-focused' : '',
isHovered ? 'btn-hovered' : '',
isActive ? 'btn-active' : '',
className,
].filter(Boolean).join(' ');
return (
<button
type={type}
className={buttonClasses}
disabled={isDisabled || isLoading}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
aria-label={ariaLabel}
aria-expanded={ariaExpanded}
aria-controls={ariaControls}
>
{isLoading ? (
<span className="btn-spinner" aria-hidden="true">
Loading...
</span>
) : (
children
)}
</button>
);
};
export default Button;
The hook returns:
- State: All button state properties (isLoading, isDisabled, etc.)
- Event handlers: All necessary event handlers for proper interactivity
- Computed properties: The final computed classes and state values
Key state transitions:
- Loading state changes when
loadingprop changes - Disabled state changes when
disabledprop changes - Interactive states (hover, focus, active) change based on user interaction events
- Async operations update loading state with optimistic updates
The implementation handles:
- Proper async operation handling with loading states
- Keyboard and mouse accessibility support
- Focus management for screen readers
- Visual feedback for all interactive states
- Clean separation of concerns between state and presentation
4. Routing & Navigation
Route Structure and Hierarchy
The Button component is a reusable UI primitive that doesn't require dedicated routing as it's a presentational component. However, if this were part of a larger feature system that includes button usage in navigation contexts (e.g., primary action buttons on pages), the route structure would look like:
graph TD
A[Root Route] --> B[Dashboard]
A --> C[User Profile]
A --> D[Settings]
B --> E[Analytics]
B --> F[Reports]
C --> G[Edit Profile]
D --> H[Account Settings]
D --> I[Notification Preferences]
Code Splitting Strategy
Since the Button component is a lightweight presentational component, it doesn't benefit from route-based code splitting. However, if we were to implement a feature that uses buttons for navigation:
- Route-based code splitting would apply to feature routes (e.g.,
/dashboard,/settings) that contain multiple button variants - Component-based code splitting would apply to the Button component itself if we had multiple button styles that are rarely used together (e.g.,
ButtonwithIcon,ButtonwithBadge, etc.)
Navigation Guards and Authentication Flows
The Button component itself doesn't implement navigation guards. However, when used in navigation contexts:
- Buttons that navigate to protected routes should check authentication state
- Buttons with
hrefortoprops should handle redirect logic when auth fails - Implementation would use React Router's
useNavigatehook with auth context checks
Deep Linking Support and URL Parameter Design
The Button component supports deep linking through:
hrefprop for external navigationtoprop for internal routing (via React Router)targetprop for new tab/window behavior
URL parameters would be handled by the parent route components that use the Button, not the Button itself.
Browser History Management and Scroll Restoration
The Button component doesn't directly manage browser history. When used for navigation:
- React Router handles scroll restoration automatically
- Custom scroll behavior can be implemented via
useScrollRestorationhook if needed - Buttons with
toprop should maintain proper browser history stack
// Example usage in route context
const ButtonWithNavigation = () => {
const navigate = useNavigate();
const handleNavigate = () => {
// React Router handles history and scroll restoration
navigate('/dashboard');
};
return (
<Button
onClick={handleNavigate}
variant="primary"
>
Go to Dashboard
</Button>
);
};
5. API Integration Layer
API client design pattern
For a reusable Button component, we don't typically make direct API calls. However, if the button triggers API operations (like submitting a form or fetching data), we'll use a centralized API client pattern. We'll implement a fetch-based wrapper with axios-like capabilities:
// api/client.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
class ApiClient {
private baseURL: string;
private defaultConfig: AxiosRequestConfig;
constructor(baseURL: string, defaultHeaders?: Record<string, string>) {
this.baseURL = baseURL;
this.defaultConfig = {
baseURL,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
},
};
}
async request<T>(
config: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
const mergedConfig = { ...this.defaultConfig, ...config };
return axios(mergedConfig);
}
get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.request<T>({ ...config, method: 'GET', url });
}
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.request<T>({ ...config, method: 'POST', url, data });
}
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.request<T>({ ...config, method: 'PUT', url, data });
}
delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.request<T>({ ...config, method: 'DELETE', url });
}
}
export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000');
Request/response interceptors
We'll implement request and response interceptors for consistent auth token handling, error normalization, and response transformation:
// api/interceptors.ts
import { apiClient } from './client';
// Token management (mocked for this example)
let authToken: string | null = null;
apiClient.request.interceptors.request.use(
(config) => {
// Add auth token to all requests
if (authToken) {
config.headers = {
...config.headers,
Authorization: `Bearer ${authToken}`,
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
apiClient.request.interceptors.response.use(
(response) => {
// Normalize successful responses
return response.data;
},
(error) => {
// Handle common error cases
if (error.response?.status === 401) {
// Handle unauthorized access - potentially redirect to login
window.location.href = '/login';
}
// Normalize error responses for consistent handling
const normalizedError = {
status: error.response?.status,
message: error.response?.data?.message || error.message,
code: error.response?.data?.code,
};
return Promise.reject(normalizedError);
}
);
Error handling and retry strategy
For API operations triggered by the button, we implement a retry mechanism with exponential backoff:
// api/retry.ts
type RetryConfig = {
maxRetries?: number;
backoffMultiplier?: number;
initialDelay?: number;
};
const defaultRetryConfig: RetryConfig = {
maxRetries: 3,
backoffMultiplier: 2,
initialDelay: 1000,
};
export async function withRetry<T>(
operation: () => Promise<T>,
config: RetryConfig = defaultRetryConfig
): Promise<T> {
const { maxRetries, backoffMultiplier, initialDelay } = {
...defaultRetryConfig,
...config,
};
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff
const delay = initialDelay * Math.pow(backoffMultiplier, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Loading, error, and empty states design
For button-triggered API operations, we maintain a consistent state management approach:
// types/api.ts
export type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
export type ApiRequestConfig = {
retry?: boolean;
retryConfig?: RetryConfig;
};
Data fetching strategy
Since the Button component itself doesn't fetch data, we focus on how it would integrate with data fetching patterns:
- CSR (Client-Side Rendering): Most appropriate for interactive button actions that trigger API calls
- SSR: Not relevant for button components, but could be used for pre-fetched data that the button operates on
- SSG: Not applicable for dynamic button actions
API response transformation layer
When the button triggers data operations, we transform responses to domain models:
// api/transformers.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface UserResponse {
id: string;
name: string;
email: string;
}
export function transformUserResponse(response: UserResponse): User {
return {
id: response.id,
name: response.name,
email: response.email,
};
}
Sequence diagram for primary data flows
sequenceDiagram
participant Button
participant ApiClient
participant Server
participant Store
Button->>ApiClient: onClick() triggers API call
ApiClient->>Server: POST /api/users
Server-->>ApiClient: Response (201 Created)
ApiClient-->>Button: Success callback
Button->>Store: Dispatch success action
Store-->>Button: Updated state
Button->>User: Re-render with new data
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for all button variants
- Interaction to Next Paint (INP): <200ms for button clicks
- Cumulative Layout Shift (CLS): <0.1 for all button states
Bundle Size Budget
- Button component bundle: <2KB (minified + gzipped)
- All button variants combined: <3KB total
- CSS bundle size: <1KB (minified + gzipped)
- No external dependencies beyond React and React DOM
Critical Rendering Path Optimization
// Button component with minimal critical rendering
const Button = React.memo(({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
...props
}: ButtonProps) => {
// Critical rendering: minimal DOM structure
return (
<button
type="button"
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
{...props}
>
{children}
</button>
);
});
Image Optimization Strategy
- No image dependencies - all buttons use CSS for styling
- SVG icons: inline SVGs with proper viewBox attributes
- Font icons: CSS-based icon rendering (no external font loading)
- Preloaded: Critical CSS for button variants via preload tags
Font Loading Strategy
<!-- Preload critical font subsets -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/inter-bold.woff2" as="font" type="font/woff2" crossorigin>
Memoization Strategy
// Critical memoization patterns for Button component
const Button = React.memo(({
children,
variant,
size,
disabled,
onClick,
className,
...rest
}: ButtonProps) => {
// Memoize computed styles to avoid re-renders
const computedStyles = useMemo(() => {
return {
backgroundColor: getBackgroundColor(variant, disabled),
color: getTextColor(variant, disabled),
padding: getPadding(size),
fontSize: getFontSize(size)
};
}, [variant, size, disabled]);
// Memoize event handlers to prevent unnecessary re-renders
const handleClick = useCallback((e: React.MouseEvent) => {
if (!disabled && onClick) {
onClick(e);
}
}, [disabled, onClick]);
return (
<button
type="button"
className={className}
style={computedStyles}
onClick={handleClick}
disabled={disabled}
{...rest}
>
{children}
</button>
);
}, (prevProps, nextProps) => {
// Custom equality check to prevent unnecessary re-renders
return (
prevProps.variant === nextProps.variant &&
prevProps.size === nextProps.size &&
prevProps.disabled === nextProps.disabled &&
prevProps.className === nextProps.className
);
});
Virtualization for Large Lists
- Not applicable - Button component is a single element, not a list
- For button groups: Use
React.memofor individual buttons in lists
Prefetching and Preloading Strategy
// Button component with prefetching strategy
const Button = ({
children,
variant,
size,
disabled,
onClick,
href,
prefetch = false
}: ButtonProps) => {
// Prefetch related resources when button is likely to be clicked
useEffect(() => {
if (prefetch && href) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = href;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}
}, [prefetch, href]);
return (
<button
type="button"
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
Performance Monitoring
- LCP tracking: Measure button interaction timing
- INP tracking: Monitor click response times
- CLS tracking: Ensure no layout shifts from button state changes
- Bundle size monitoring: Track component bundle size over time
Memory Optimization
- No memory leaks in event handlers or timers
- Proper cleanup of DOM event listeners
- Minimal state management - only necessary internal state (loading, disabled)
- React.memo prevents unnecessary re-renders for same props
Bundle Analysis Strategy
{
"Button": {
"size": "2.1KB",
"dependencies": 0,
"gzip": "0.8KB",
"brotli": "0.6KB"
}
}
Critical Path Optimization
- CSS-in-JS: Inline critical styles to reduce FOUC
- Critical CSS: Extract and inline essential button styles
- Async loading: Non-critical button variants load async when needed
- Code splitting: Button variants can be split into separate chunks if needed
Rendering Optimization
- Minimal DOM nodes: Single button element per component instance
- Static class names: No dynamic class name generation that causes reflows
- CSS transitions: Use hardware-accelerated CSS properties (transform, opacity)
- Avoid forced synchronous layouts: No layout thrashing in event handlers
Accessibility Performance
- No performance impact on screen readers or keyboard navigation
- Fast focus management: No delays in focus transitions
- Quick state updates: Button state changes (loading, disabled) happen instantly
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. For a Button component, we primarily need to consider:
- Component-level error boundaries: Wrap the Button in an ErrorBoundary component to catch rendering issues from props or internal state
- Parent container boundaries: If Button is part of a form or modal, the parent container should handle broader error states
- Global error boundary: For uncaught errors in the application context
graph TD
A[App] --> B[ErrorBoundary]
B --> C[Button]
B --> D[Form]
D --> E[ButtonGroup]
Global Error Handler Design
Implement a global error handler that captures unhandled promise rejections and uncaught exceptions:
// Global error handler setup in app entry point
const globalErrorHandler = (error: Error, errorInfo: React.ErrorInfo) => {
// Log to monitoring service
Sentry.captureException(error, {
contexts: { react: errorInfo }
});
// Track in analytics
trackEvent('unhandled_error', {
error: error.message,
componentStack: errorInfo.componentStack
});
// Show user-friendly message in production
if (process.env.NODE_ENV === 'production') {
// Update global state or show toast
}
};
// Register at app root
window.addEventListener('error', (event) => {
globalErrorHandler(event.error, { componentStack: '' });
});
window.addEventListener('unhandledrejection', (event) => {
globalErrorHandler(event.reason, { componentStack: '' });
});
Offline / Network Failure Handling
For Buttons that trigger network operations:
// Button component with offline handling
const OfflineButton: React.FC<ButtonProps> = ({
onClick,
disabled,
...props
}) => {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const handleClick = (e: React.MouseEvent) => {
if (!isOnline) {
// Show offline toast or modal
showToast('Offline - Please check your connection', 'warning');
return;
}
onClick?.(e);
};
return (
<button
{...props}
disabled={disabled || !isOnline}
onClick={handleClick}
>
{props.children}
</button>
);
};
Graceful Degradation Patterns
Buttons should degrade gracefully when features aren't available:
// Button with feature detection and fallbacks
const FeatureAwareButton: React.FC<ButtonProps> = ({
onClick,
variant = 'primary',
...props
}) => {
const [hasTouch, setHasTouch] = useState(false);
useEffect(() => {
// Check for touch support for mobile-friendly fallbacks
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
setHasTouch(isTouchDevice);
// Check for required DOM APIs
const hasPointerEvents = typeof CSS !== 'undefined' &&
CSS.supports('touch-action', 'manipulation');
if (!hasPointerEvents) {
// Fallback for older browsers
console.warn('Touch interaction may be limited');
}
}, []);
return (
<button
{...props}
onClick={(e) => {
// Fallback for browsers without proper pointer support
if (hasTouch && !('pointerType' in e)) {
// Handle touch-specific behavior
}
onClick?.(e);
}}
className={`
${variant === 'primary' ? 'bg-blue-500' : 'bg-gray-200'}
${hasTouch ? 'touch-action-manipulation' : ''}
${props.disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
{props.children}
</button>
);
};
User Feedback on Errors
Implement toast notifications and inline error states:
// Toast notification system for button errors
const ToastProvider: React.FC = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (message: string, type: 'error' | 'warning' | 'success') => {
const id = Date.now().toString();
setToasts(prev => [...prev, { id, message, type }]);
// Auto-remove after timeout
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 5000);
};
return (
<>
{children}
<div className="fixed top-4 right-4 space-y-2 z-50">
{toasts.map(toast => (
<Toast key={toast.id} {...toast} />
))}
</div>
</>
);
};
const Toast: React.FC<Toast> = ({ message, type }) => {
const bgColor = type === 'error' ? 'bg-red-500' :
type === 'warning' ? 'bg-yellow-500' : 'bg-green-500';
return (
<div className={`p-3 rounded shadow-lg text-white ${bgColor}`}>
{message}
</div>
);
};
Error Recovery Patterns
Buttons should support recovery from transient failures:
// Retry-capable button with error recovery
const RetryButton: React.FC<{
onClick: () => Promise<void>;
retryCount?: number;
retryDelay?: number;
}> = ({ onClick, retryCount = 3, retryDelay = 1000 }) => {
const [retryAttempts, setRetryAttempts] = useState(0);
const [error, setError] = useState<string | null>(null);
const handleClick = async () => {
try {
setError(null);
await onClick();
setRetryAttempts(0); // Reset on success
} catch (err) {
if (retryAttempts < retryCount) {
setRetryAttempts(prev => prev + 1);
setTimeout(() => {
// Retry logic with exponential backoff
handleClick();
}, retryDelay * Math.pow(2, retryAttempts));
} else {
setError('Operation failed after maximum retries');
// Log to monitoring service
Sentry.captureException(err);
}
}
};
return (
<button
onClick={handleClick}
disabled={retryAttempts > 0}
className="relative"
>
{retryAttempts > 0 ? (
<span className="flex items-center">
<Spinner size="sm" />
<span className="ml-2">Retrying...</span>
</span>
) : (
'Retry'
)}
{error && (
<div className="absolute -bottom-6 left-0 text-red-500 text-sm">
{error}
</div>
)}
</button>
);
};
Error Boundary Implementation
// Custom error boundary for Button components
class ButtonErrorBoundary extends React.Component<{
fallback?: React.ReactNode;
}, { hasError: boolean }> {
constructor(props: { fallback?: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error for debugging
console.error('Button component error:', error, errorInfo);
// Send to monitoring service
Sentry.captureException(error, {
contexts: { react: errorInfo }
});
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-100 border border-red-300 rounded">
<p className="text-red-700">Something went wrong with this button.</p>
<button
onClick={() => window.location.reload()}
className="mt-2 px-3 py-1 bg-red-500 text-white rounded"
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
8. Testing Strategy
Unit Testing Approach
Component Tests (70% coverage target):
- Test all button variants (primary, secondary, tertiary, etc.) with different props combinations
- Verify proper rendering of icon-only buttons and text + icon combinations
- Test all interactive states (hover, focus, active, disabled)
- Validate proper TypeScript prop validation and type checking
- Test accessibility attributes (aria-label, role) for screen readers
Utility Tests (20% coverage target):
- Test button size calculation logic for responsive design
- Validate icon positioning and sizing calculations
- Test theme color mapping functions
Mock Coverage:
// Mock dependencies for unit tests
const mockTheme = {
colors: { primary: '#007bff', secondary: '#6c757d' },
spacing: { xs: '4px', sm: '8px' },
breakpoints: { mobile: '320px', tablet: '768px' }
};
Integration Testing Strategy
Component Interaction Tests (50% coverage):
- Test button click handlers with mock callbacks
- Validate proper event propagation and bubbling behavior
- Test integration with form submission contexts
- Verify proper focus management in accessible scenarios
State Integration Tests (30% coverage):
- Test button state transitions (loading → idle → success/failure)
- Validate proper disabled state behavior during async operations
- Test concurrent button usage patterns
E2E Testing Scope
Cypress-based E2E Coverage:
- End-to-end user journey testing for primary button flows (e.g., form submission, navigation)
- Cross-browser compatibility testing (Chrome, Firefox, Safari)
- Mobile/responsive behavior validation
- Keyboard navigation end-to-end testing
Critical User Flows:
- Button click with success state transition
- Loading state during async operation
- Focus management in modal contexts
- Accessibility compliance validation
Visual Regression Testing
Tool: Percy (or similar visual testing service)
- Test all button variants across different themes and breakpoints
- Validate icon rendering consistency
- Capture state transitions (hover, focus, active)
- Monitor for visual regressions in responsive layouts
Accessibility Testing Automation
Axe-core Integration:
- Run automated accessibility checks on all button variants
- Validate proper ARIA attribute combinations
- Test keyboard navigation compliance
- Monitor for WCAG 2.1 AA level violations
Test Data Strategy
Test Fixtures:
// Button test data fixtures
const buttonVariants = [
{ variant: 'primary', color: '#007bff' },
{ variant: 'secondary', color: '#6c757d' },
{ variant: 'tertiary', color: '#ffffff' }
];
const buttonStates = [
{ state: 'idle', disabled: false },
{ state: 'loading', disabled: true },
{ state: 'disabled', disabled: true }
];
Test Coverage Targets:
- 90% code coverage for component logic
- 85% branch coverage for conditional rendering paths
- 100% accessibility attribute compliance
- 95% visual regression test pass rate
Test Environment:
- Jest for unit testing with React Testing Library
- Cypress for E2E testing with cross-browser support
- Percy for visual regression testing with CI integration
- axe-core for automated accessibility validation in CI pipeline
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The Button component must meet the following WCAG 2.1 AA success criteria:
- 1.1.1 Non-text Content: All buttons must have accessible names via
aria-labeloraria-labelledbywhen using only icons - 1.3.1 Info and Relationships: Proper semantic HTML structure with
role="button"when needed - 1.4.1 Use of Color: Sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
- 1.4.13 Text Spacing: Support for custom spacing without breaking layout
- 2.1.1 Keyboard: Full keyboard navigation support (Tab, Enter, Space)
- 2.4.1 Bypass Blocks: Buttons that navigate should have skip links or clear landmarks
- 2.4.6 Headings and Labels: All buttons must have meaningful labels or roles
- 3.1.1 Language: Support for language attributes when content is in different languages
- 4.1.2 Name, Role, Value: Proper ARIA attributes and roles for all interactive states
Keyboard navigation support
The Button component must be fully keyboard accessible:
- Tab Navigation: Buttons receive focus in the natural tab order
- Activation Keys: Space and Enter keys activate buttons when focused
- Focus Management: Proper focus ring styling that meets WCAG contrast requirements
- Focus Trapping: When used in modals or dropdowns, focus is properly managed
// Example keyboard event handling in Button component
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick?.(e);
}
};
Focus management strategy
Focus management is critical for accessible buttons:
- Focus Trapping: When a button opens a dropdown or modal, focus is trapped within the component until closed
- Focus Restoration: When modals close, focus returns to the previously focused element
- Visual Focus Indicators: Custom focus styles that meet contrast requirements (minimum 3:1 ratio)
- Focus Outline: Use
outlineproperty with sufficient contrast instead of hiding focus styles
// Focus management in Button component
const [isFocused, setIsFocused] = useState(false);
const handleFocus = () => setIsFocused(true);
const handleBlur = () => setIsFocused(false);
return (
<button
{...props}
onFocus={handleFocus}
onBlur={handleBlur}
className={`
${isFocused ? 'focus-visible:ring-2 focus-visible:ring-blue-500' : ''}
${className}
`}
>
{children}
</button>
);
ARIA roles and attributes plan
The Button component will use appropriate ARIA attributes:
aria-label: For icon-only buttons to provide meaningful contextaria-describedby: For additional descriptive text when neededaria-expanded: For toggle buttons or dropdown triggersaria-pressed: For toggle buttons in pressed staterole="button": When using non-button elements as buttons (e.g.,divorspan)aria-disabled: When button is in disabled state to prevent screen reader confusion
Screen reader compatibility
The Button component must provide optimal screen reader support:
- Semantic HTML: Use native
<button>elements when possible - Descriptive Labels: All buttons must have clear, descriptive text or ARIA labels
- State Indicators: Screen readers must announce button state changes (e.g., "button pressed", "loading")
- Dynamic Content Updates: When button state changes dynamically, screen readers receive appropriate announcements
- Landmark Support: Buttons that navigate or change context should be properly labeled with ARIA landmarks
// Screen reader accessible button implementation
const Button = ({
children,
ariaLabel,
ariaExpanded,
ariaPressed,
disabled,
loading,
...props
}: ButtonProps) => {
const buttonAriaLabel = ariaLabel || (typeof children === 'string' ? children : undefined);
return (
<button
aria-label={buttonAriaLabel}
aria-expanded={ariaExpanded}
aria-pressed={ariaPressed}
disabled={disabled || loading}
{...props}
>
{loading ? (
<span className="sr-only">Loading...</span>
) : (
children
)}
</button>
);
};
Additional accessibility considerations
- High Contrast Mode Support: Buttons must work in Windows High Contrast Mode
- Reduced Motion Support: Respect
prefers-reduced-motionmedia queries for animations - Touch Target Size: Buttons must meet minimum 44px touch target size requirements for mobile
- Dynamic Text Resizing: Buttons should support text resizing up to 200% without breaking layout
- Color Blindness: Ensure color contrast meets AA standards for all users, including those with color vision deficiency
10. Security Considerations
XSS Prevention Strategy
The Button component must prevent cross-site scripting attacks by sanitizing all dynamic content and preventing DOM-based XSS:
// Secure button implementation with XSS protection
const Button = ({
children,
onClick,
href,
disabled,
...props
}: ButtonProps) => {
// Sanitize children to prevent XSS
const sanitizedChildren = typeof children === 'string'
? sanitizeHtml(children) // Custom sanitizer or use DOMPurify
: children;
const handleClick = (e: React.MouseEvent) => {
if (disabled) return;
// Prevent direct DOM manipulation in event handlers
e.preventDefault();
// Validate and sanitize any dynamic content before passing to onClick
if (typeof onClick === 'function') {
// Ensure event data is properly sanitized before use
onClick(e);
}
};
if (href) {
// Prevent JavaScript protocol in href
const safeHref = href.startsWith('javascript:') ? '#' : href;
return (
<a
href={safeHref}
onClick={handleClick}
{...props}
dangerouslySetInnerHTML={undefined} // Prevent direct HTML injection
>
{sanitizedChildren}
</a>
);
}
return (
<button
type="button"
onClick={handleClick}
disabled={disabled}
{...props}
>
{sanitizedChildren}
</button>
);
};
CSRF Protection
For buttons that trigger destructive actions or form submissions:
// Button with CSRF token handling
const SecureButton = ({
csrfToken,
actionUrl,
method = 'POST',
...props
}: SecureButtonProps) => {
const handleClick = async (e: React.MouseEvent) => {
e.preventDefault();
// Validate CSRF token exists and is valid
if (!csrfToken) {
throw new Error('CSRF token missing');
}
// Use token in request headers or form data
const response = await fetch(actionUrl, {
method,
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
// ... other request options
});
// Handle response appropriately
};
return (
<button
onClick={handleClick}
{...props}
>
{props.children}
</button>
);
};
Content Security Policy (CSP) Headers
The frontend must respect CSP policies that prevent inline JavaScript execution:
// Button component that works within CSP constraints
const CSPCompliantButton = ({
onClick,
href,
...props
}: ButtonProps) => {
// Avoid inline event handlers - use proper DOM event listeners
const handleClick = (e: React.MouseEvent) => {
// This approach works with CSP restrictions
if (typeof onClick === 'function') {
onClick(e);
}
};
// Use proper DOM attributes instead of inline styles/JS
return (
<button
onClick={handleClick}
className="csp-safe-button"
{...props}
>
{props.children}
</button>
);
};
Sensitive Data Handling in Frontend State
The component should not store or expose sensitive data:
// Button that avoids storing sensitive props in state
const SecureButton = ({
// Avoid storing sensitive data in component state
sensitiveData, // ❌ Don't store this in state or pass to child components
...restProps
}: ButtonProps) => {
// Instead, pass sensitive data through props or context
return (
<button
{...restProps}
// Never store sensitive data in DOM attributes or state
>
{restProps.children}
</button>
);
};
Input Sanitization Approach
All dynamic content should be sanitized before rendering:
// Input sanitization for button text and attributes
const sanitizeButtonContent = (content: string): string => {
// Remove potentially dangerous patterns
return content
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
};
const Button = ({
children,
title,
ariaLabel,
...props
}: ButtonProps) => {
const safeChildren = sanitizeButtonContent(String(children));
const safeTitle = sanitizeButtonContent(String(title || ''));
return (
<button
title={safeTitle}
aria-label={sanitizeButtonContent(String(ariaLabel || safeChildren))}
{...props}
>
{safeChildren}
</button>
);
};
Security Best Practices Implementation
-
Never use
dangerouslySetInnerHTML- This is a critical security anti-pattern that should never be used in the Button component. -
Validate all props - All dynamic props must be validated before use:
const validateButtonProps = (props: ButtonProps) => {
// Validate href format
if (props.href && !isValidUrl(props.href)) {
throw new Error('Invalid href URL');
}
// Validate button type
if (props.type && !['button', 'submit', 'reset'].includes(props.type)) {
throw new Error('Invalid button type');
}
};
- Prevent URL redirection attacks - Validate and sanitize all href values:
const isValidUrl = (url: string): boolean => {
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
};
Security Testing Considerations
The component should be tested against:
- XSS injection attacks in button text
- Malicious URL redirection attempts
- CSRF token validation failures
- Inline script execution prevention
- Attribute sanitization for dangerous content
Risk Mitigations
- Runtime validation - All props should be validated at runtime
- Security audits - Regular code reviews for security vulnerabilities
- Dependency scanning - Monitor for security issues in third-party libraries
- Content security policy enforcement - Ensure CSP headers are properly configured and enforced
Security Headers Considerations
The component should not rely on client-side security headers but should work within the security framework established by the application's CSP, XSS protection, and CSRF middleware.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys will follow a hierarchical, descriptive naming convention to ensure maintainability and clarity:
components.button.{variant}.{state}
Example keys:
components.button.primary.defaultcomponents.button.secondary.hovercomponents.button.disabledcomponents.button.loading
RTL Support Considerations
RTL (Right-to-Left) support will be handled through CSS class toggling based on the current locale direction. The component will:
- Apply
dir="rtl"to the root element when RTL is active - Use CSS logical properties (e.g.,
margin-inline-startinstead ofmargin-left) - Implement proper text alignment and icon positioning for RTL layouts
- Use CSS variables to dynamically adjust spacing and layout in RTL context
Date, Number, Currency Formatting Strategy
For any date or numeric content within buttons (e.g., countdown timers, badge counts):
- Use
Intl.DateTimeFormatfor date formatting - Use
Intl.NumberFormatfor numeric values (with proper locale-aware formatting) - Apply appropriate currency formatting when displaying prices or costs
- Format numbers with proper grouping separators and decimal points per locale
Locale Detection and Switching Strategy
Automatic detection:
- Primary detection via
navigator.languageornavigator.languages - Fallback to default locale (
en-US) if no browser language detected - Support for user preference persistence in localStorage
Manual switching:
- Language selector component with flag icons
- URL parameter (
?lang=es-ES) for deep linking to specific locales - Session storage persistence across page reloads
Implementation approach:
// i18n.ts
export const getLocale = (): string => {
return localStorage.getItem('locale') ||
navigator.language ||
'en-US';
};
export const setLocale = (locale: string): void => {
localStorage.setItem('locale', locale);
document.documentElement.lang = locale;
};
The button component will automatically re-render when the locale changes, ensuring all text content updates appropriately with proper translation keys and formatting.
12. Monitoring & Observability
Real User Monitoring (RUM) metrics to track
For a reusable Button component, the following RUM metrics should be tracked:
- Click Frequency: Track how often each button variant is clicked to understand user behavior patterns and identify high-traffic actions
- Click Latency: Measure time from button press to action completion to identify performance bottlenecks in button interactions
- Success Rate: Monitor successful vs. failed button actions to detect issues with specific button types or workflows
- User Journey Completion: Track button clicks as part of user flows to understand how buttons contribute to feature adoption
- Accessibility Metrics: Track keyboard navigation success rates and screen reader interactions with buttons
Error tracking integration (Sentry / Datadog)
Integrate with Sentry for comprehensive error monitoring:
// Button component error tracking integration
import * as Sentry from '@sentry/react';
const Button = ({
onClick,
variant = 'primary',
disabled = false,
...props
}: ButtonProps) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
try {
// Track button click event
if (typeof window !== 'undefined') {
window.gtag?.('event', 'button_click', {
event_category: 'engagement',
event_label: variant,
timestamp: Date.now()
});
}
// Execute user-provided handler
onClick?.(e);
} catch (error) {
// Report errors to Sentry with context
Sentry.captureException(error, {
tags: {
component: 'Button',
variant,
action: 'click_handler'
},
extra: {
buttonProps: props,
timestamp: Date.now()
}
});
throw error;
}
};
return (
<button
onClick={handleClick}
disabled={disabled}
{...props}
>
{children}
</button>
);
};
Custom events and user journey tracking
Implement custom event tracking for button interactions:
// Button analytics tracking
const trackButtonEvent = (eventData: {
action: string;
variant?: string;
context?: string;
userId?: string;
}) => {
const { action, variant, context, userId } = eventData;
// Send to analytics service
if (typeof window !== 'undefined') {
const eventPayload = {
event: 'button_interaction',
action,
variant,
context,
userId,
timestamp: Date.now(),
pageUrl: window.location.href,
userAgent: navigator.userAgent
};
// Example: Google Analytics 4
if (window.gtag) {
window.gtag('event', 'button_interaction', {
...eventPayload,
event_category: 'engagement',
event_label: `${action}_${variant || 'default'}`
});
}
}
};
Performance monitoring and regression detection
Implement performance monitoring:
// Performance monitoring for button interactions
const useButtonPerformance = (variant: ButtonVariant) => {
const [performanceMetrics, setPerformanceMetrics] = useState({
avgClickLatency: 0,
clickCount: 0,
errorRate: 0
});
const trackClickPerformance = useCallback((startTime: number) => {
const latency = Date.now() - startTime;
setPerformanceMetrics(prev => ({
avgClickLatency: (prev.avgClickLatency * prev.clickCount + latency) / (prev.clickCount + 1),
clickCount: prev.clickCount + 1,
errorRate: prev.errorRate // Would be updated on error
}));
// Alert on performance degradation
if (latency > 1000) { // 1 second threshold
console.warn(`Button click latency exceeded threshold: ${latency}ms`);
}
}, []);
return { performanceMetrics, trackClickPerformance };
};
Alerting and dashboard configuration
Configure monitoring dashboards with:
- Click Rate Trends: Hourly/daily button click volume by variant
- Error Rate Thresholds: Alert when error rate exceeds 5% for any button variant
- Latency Distribution: Track 90th percentile click response times
- User Journey Drop-offs: Identify buttons where users abandon workflows
Integration with backend monitoring
// Button telemetry integration with backend
const sendButtonTelemetry = async (telemetryData: {
buttonId?: string;
variant: string;
action: string;
duration?: number;
success?: boolean;
error?: string;
}) => {
try {
const response = await fetch('/api/telemetry/button', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...telemetryData,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`
})
});
if (!response.ok) {
throw new Error(`Telemetry send failed: ${response.status}`);
}
} catch (error) {
// Fallback to local storage for retry
const failedTelemetry = {
data: telemetryData,
timestamp: Date.now(),
retryCount: 0
};
localStorage.setItem('failed_button_telemetry', JSON.stringify(failedTelemetry));
}
};
Data retention and privacy considerations
- Retain telemetry data for 90 days for performance analysis
- Anonymize user identifiers in telemetry
- Comply with GDPR/CCPA for data collection and processing
- Implement opt-out mechanisms for telemetry collection where required
Dashboard visualization strategy
Create dashboards with:
- Real-time click volume by button variant
- Error rate trends over time
- Performance distribution charts (latency, success rates)
- User journey flow maps showing button participation in workflows
13. File & Folder Structure
Proposed Directory Structure
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.types.ts
│ │ ├── Button.module.scss
│ │ └── index.ts
├── features/
│ └── dashboard/
│ ├── Dashboard.tsx
│ └── index.ts
├── shared/
│ ├── hooks/
│ │ └── useTheme.ts
│ ├── utils/
│ │ └── formatCurrency.ts
│ └── types/
│ └── theme.ts
└── index.ts
Module Boundaries and Import Rules
- Shared components (like Button) live in
src/components/with barrel exports - Feature-specific components are nested under their respective feature folders (
features/dashboard/) - No cross-feature imports - components should only import from shared or their own feature
- Barrel exports provide clean public APIs:
import { Button } from 'components/Button'
Shared vs Feature-Specific Code Separation
- Shared code (
components/Button) is generic, reusable across features - Feature-specific code lives in feature folders and can import shared components
- Theme system is shared but implemented per-feature when needed (e.g., dashboard overrides)
- Utilities and types are shared across the codebase but feature-specific logic stays in features
This structure enables:
- Clear ownership boundaries
- Easy component reusability
- Maintainable code organization
- Scalable feature development without cross-feature coupling
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React (v18+)
- Justification: Core library for building user interfaces with component-based architecture. Essential for any React-based application and provides hooks, context, and concurrent rendering capabilities.
- Bundle impact: ~100KB (minified + gzip)
- Alternatives considered: Preact - smaller bundle size but less ecosystem support and community adoption
2. TypeScript (v5+)
- Justification: Provides static type checking that catches errors at compile time, improves developer experience with better IDE support, and enables better code documentation through type definitions.
- Bundle impact: None (compile-time only)
- Alternatives considered: Flow - less adoption in the React ecosystem, more complex setup
3. Styled Components / Tailwind CSS
- Justification: For styling approach, Tailwind CSS is selected due to its utility-first approach that enables rapid UI development and consistent design system implementation. Provides better maintainability through design tokens.
- Bundle impact: ~10KB (Tailwind base + custom config)
- Alternatives considered: CSS Modules - more verbose, Styled Components - larger bundle size and less predictable performance
4. React Hook Form (v7+)
- Justification: For form handling in button-based forms, provides optimized validation and error handling without unnecessary re-renders. When used with button components in forms, this integration is crucial.
- Bundle impact: ~5KB (minified + gzip)
- Alternatives considered: Formik - more complex setup, less performant for simple use cases
5. React Testing Library (v13+)
- Justification: Provides testing patterns that align with user interactions, making tests more maintainable and focused on behavior rather than implementation details.
- Bundle impact: None (dev dependency)
- Alternatives considered: Enzyme - more complex API, less reliable for React 18
Alternatives Considered and Rejected
1. Emotion CSS-in-JS Library
- Rejected because: While powerful, it introduces complexity in debugging and increases bundle size. Tailwind's utility-first approach provides better performance characteristics for the button component use case.
2. Material UI Components
- Rejected because: Overkill for a simple button component and introduces unnecessary dependencies and complexity. The design system should be handled separately.
3. Styled Components
- Rejected because: While it provides great CSS-in-JS capabilities, the bundle size overhead and complexity of managing styled components outweigh the benefits for a simple button component.
Licensing and Bundle Size Impact Assessment
License Compliance: All selected libraries are MIT or BSD-3-Clause licensed, which allows for commercial use and open-source distribution without significant restrictions.
Bundle Size Impact Summary:
- Core React + TypeScript: ~100KB (minified)
- Tailwind CSS: ~10KB (base + custom config)
- React Hook Form: ~5KB
- React Testing Library: ~0KB (dev dependency)
- Total estimated runtime bundle impact: ~115KB (minified + gzip)
Bundle Size Optimization Strategy:
- Tree-shaking enabled for all libraries
- Selective imports to minimize bundle size
- Tailwind's purge configuration to remove unused styles
- Development vs. production builds with different configurations
Third-Party Integration Considerations
1. Accessibility Integration
- The component design will integrate with
@testing-library/jest-domfor accessibility testing - Will support integration with
axe-corefor automated accessibility checks
2. Performance Monitoring
- Will integrate with
react-inspectorfor debugging in development mode - Compatible with performance monitoring tools like Lighthouse and Web Vitals
3. Internationalization Support
- Compatible with
i18nextandreact-i18nextfor translation support in button labels
The selected dependencies provide a balance between developer experience, performance, maintainability, and feature completeness for building a reusable button component that can scale across different applications and design systems.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Button variant consistency: Should all button variants (primary, secondary, tertiary) follow the same visual hierarchy and spacing system? This requires design system alignment to prevent visual inconsistency across teams.
- Accessibility compliance levels: Should the component enforce strict WCAG 2.1 AA compliance or allow teams to configure different levels based on their accessibility requirements?
- Icon placement strategy: Should icons be strictly left/right aligned or support flexible positioning (e.g., inline, stacked) that requires more complex layout logic?
Known trade-offs made in this design
- Bundle size vs. flexibility: Prioritized component flexibility over bundle size by including all variant props in the main component rather than creating separate components for each variant, which would reduce bundle size but increase maintenance overhead.
- Type safety vs. developer experience: Used union types for variant and size props instead of strict enums to allow extensibility while maintaining type safety.
- CSS-in-JS vs. CSS modules: Chose CSS-in-JS for dynamic variant styling to avoid CSS class name conflicts but acknowledged this increases runtime cost and bundle size.
Risks and mitigations
- CSS specificity conflicts: Risk of CSS overrides from global styles or third-party libraries. Mitigation: Use CSS modules with unique class names and implement a robust design system that enforces consistent styling.
- Accessibility regression: Risk of breaking keyboard navigation or screen reader support during future updates. Mitigation: Implement comprehensive A11y tests with axe-core and maintain an accessibility regression test suite.
- Performance impact from dynamic styling: Risk of layout thrashing or reflows due to complex CSS-in-JS logic. Mitigation: Use React.memo for the component and ensure consistent DOM structure across variants.
Technical debt acknowledged
- Limited theming support: The current implementation doesn't support deep theme overrides (e.g., custom color schemes) which may require refactoring to use CSS variables or a more robust theming system.
- No automatic icon size scaling: Icons don't automatically scale with button size, requiring manual configuration which could lead to inconsistent UIs.
- Limited touch target optimization: While the component includes proper focus states, it doesn't include specific touch target optimizations for mobile devices that might require additional padding or sizing logic.
- No built-in loading state: The component doesn't include a loading spinner or disabled state that would require additional props and logic to handle user feedback during async operations.