Skip to main content

Reusable Button Component Design and Implementation

Comprehensive low-level design for a flexible, accessible, and themeable React button component with TypeScript support

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

  1. What target browsers/devices should this component support? (e.g., IE11, mobile-first, touch devices)
  2. Do we need to support internationalization (i18n) for button text and accessibility labels?
  3. Should this component support real-time state changes (e.g., loading states, disabled states) or is it primarily static?
  4. Is offline support required for button interactions (e.g., optimistic UI patterns)?
  5. What level of accessibility compliance is needed? (WCAG 2.1 AA, AAA, or just basic keyboard support?)
  6. Are there existing design system components that this should integrate with?
  7. 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:

  1. Default: Normal state with interactive behavior
  2. Loading: Shows spinner and disables interaction during async operations
  3. 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

  1. Mount: Initialize accessibility attributes, set up event listeners
  2. Update: Handle prop changes (disabled, loading state, variant)
  3. Unmount: Clean up any event listeners or subscriptions
  4. State transitions: Handle loading state changes with proper visual feedback
  5. 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:

  1. It provides a clean, predictable state management pattern
  2. It's built into React and doesn't add external dependencies
  3. It supports complex state transitions with minimal boilerplate
  4. It enables easy testing and debugging
  5. 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:

  1. Loading state changes when loading prop changes
  2. Disabled state changes when disabled prop changes
  3. Interactive states (hover, focus, active) change based on user interaction events
  4. 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., Button with Icon, Button with Badge, etc.)

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 href or to props should handle redirect logic when auth fails
  • Implementation would use React Router's useNavigate hook with auth context checks

Deep Linking Support and URL Parameter Design

The Button component supports deep linking through:

  • href prop for external navigation
  • to prop for internal routing (via React Router)
  • target prop 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 useScrollRestoration hook if needed
  • Buttons with to prop 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.memo for 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

  1. CSS-in-JS: Inline critical styles to reduce FOUC
  2. Critical CSS: Extract and inline essential button styles
  3. Async loading: Non-critical button variants load async when needed
  4. 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:

  1. Component-level error boundaries: Wrap the Button in an ErrorBoundary component to catch rendering issues from props or internal state
  2. Parent container boundaries: If Button is part of a form or modal, the parent container should handle broader error states
  3. 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:

  1. Button click with success state transition
  2. Loading state during async operation
  3. Focus management in modal contexts
  4. 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-label or aria-labelledby when 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 outline property 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 context
  • aria-describedby: For additional descriptive text when needed
  • aria-expanded: For toggle buttons or dropdown triggers
  • aria-pressed: For toggle buttons in pressed state
  • role="button": When using non-button elements as buttons (e.g., div or span)
  • 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-motion media 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

  1. Never use dangerouslySetInnerHTML - This is a critical security anti-pattern that should never be used in the Button component.

  2. 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');
  }
};
  1. 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

  1. Runtime validation - All props should be validated at runtime
  2. Security audits - Regular code reviews for security vulnerabilities
  3. Dependency scanning - Monitor for security issues in third-party libraries
  4. 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.default
  • components.button.secondary.hover
  • components.button.disabled
  • components.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-start instead of margin-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.DateTimeFormat for date formatting
  • Use Intl.NumberFormat for 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.language or navigator.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:

  1. Clear ownership boundaries
  2. Easy component reusability
  3. Maintainable code organization
  4. 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-dom for accessibility testing
  • Will support integration with axe-core for automated accessibility checks

2. Performance Monitoring

  • Will integrate with react-inspector for debugging in development mode
  • Compatible with performance monitoring tools like Lighthouse and Web Vitals

3. Internationalization Support

  • Compatible with i18next and react-i18next for 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.

Was this article helpful?

Topics in this article

More from Frontend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.