Skip to main content
A comprehensive understanding of states and state management creates the foundation for consistent, predictable, and maintainable user interfaces. This guide explains these concepts and provides practical applications for implementing them in our design system.

What Are States?

States represent the different conditions a component can exist in at any given moment. They determine how a component appears and behaves in response to user interactions, system events, or data changes.

Core Component States

All interactive components in our design system should account for these fundamental states:
StateDescription
DefaultThe initial appearance of a component before any interaction. This state establishes the component’s baseline visual identity while indicating its purpose and functionality.
HoverAppears when a cursor moves over an interactive element, providing a visual cue that the element can be interacted with.
ActiveOccurs when a button or control is being interacted with (pressed, clicked, tapped). This brief state provides immediate feedback that the system has recognized the user’s action.
FocusActivated by keyboard navigation or programmatic focus. Focus states must include a 2px focus indicator with a 3:1 contrast ratio to ensure accessibility for keyboard users and those using assistive technologies.
DisabledIndicates when an element exists but is unavailable for interaction. Disabled states should maintain the component’s visual structure while clearly communicating its unavailability.
ErrorUsed to highlight invalid inputs or system errors. Error states must include both visual indicators and clear messaging with at least a 4:1 contrast ratio to meet accessibility standards.

Additional Component States

Depending on the component’s complexity, these additional states may apply:
StateDescription
LoadingIndicates when a component is retrieving data or processing information. Loading states provide essential feedback that prevents user confusion during wait times.
Expanded/CollapsedUsed for components that can reveal or hide content, such as accordions, dropdowns, or expandable panels.
SelectedIndicates when an item within a collection has been chosen, such as items in a list, tabs, or menu options.
IndeterminateRepresents a state between checked and unchecked, primarily used in checkboxes when some (but not all) nested options are selected.

Understanding Statefulness

Statefulness describes how components remember and maintain information about their condition between interactions.

Stateful vs. Stateless Components

Stateful Components

Stateful components maintain memory of past interactions, store data that changes over time, and manage their own internal state. Examples include forms, toggles, accordions, and carousels. These components require:
  • Documentation of all possible states
  • Clear rules for transitions between states
  • Consideration of data persistence
  • Thoughtful implementation of state logic

Stateless Components

Stateless components don’t maintain internal memory, rendering based solely on their input properties. The same input will always produce the same output. Examples include buttons, labels, icons, and dividers. These components benefit from:
  • Simpler implementation
  • More predictable behavior
  • Easier testing and maintenance
  • Greater reusability

State Management Patterns

State management refers to the approaches used to control, organize, and maintain state in components and applications.

Levels of State Management

Component-Level State

State that only affects a single component should be managed within that component. This approach maintains encapsulation and simplifies reasoning about the component’s behavior.
// Example of component-level state
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="counter">
      <span>Count: {count}</span>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Shared State

When multiple components need access to the same state, consider lifting state to a common parent or using a shared state mechanism. This creates a single source of truth and prevents synchronization issues.

Application State

For state that affects large portions of the application (such as authentication, themes, or global settings), use centralized state management. This approach provides consistency across the application and simplifies complex state interactions.

State Management Approaches

Controlled vs. Uncontrolled Components

Controlled components receive their state from parent components and notify those parents of any requested changes:
// Controlled component example
function ControlledInput({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />;
}
Uncontrolled components manage their state internally, often using refs to access values:
// Uncontrolled component example
function UncontrolledInput() {
  const inputRef = useRef();

  function handleSubmit() {
    const value = inputRef.current.value;
    // Use the value...
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

State Machines

For components with complex state logic, state machines provide a formal way to define states and transitions. They help prevent invalid states and make behavior more predictable.
// Simplified state machine pattern
const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SUBMIT: 'loading' }
    },
    loading: {
      on: { SUCCESS: 'success', ERROR: 'error' }
    },
    success: {
      on: { RESET: 'idle' }
    },
    error: {
      on: { RETRY: 'loading', RESET: 'idle' }
    }
  }
};

Documenting States

Thorough documentation of states is essential for consistent implementation. For each component in our design system, document:

State Inventory Matrix

StateVisual TreatmentBehaviorAccessibility ConsiderationsCode Examples
Default[Describe appearance][Describe behavior][List accessibility features][Provide code snippet]
HoverBackground lightensCursor changes to pointerN/A:hover selector
Focus2px blue outlineResponds to keyboardFocus visible[data-state="focused"]
DisabledGray appearanceNo interactionCommunicates disabled status[data-disabled="true"]

State Transition Diagram

For complex components, include a diagram showing all possible states and transitions between them. For example, a form submission process might follow this pattern:
Idle → Validating → Submitting → Success
  ↑        ↓            ↓
  ↑        ↓            ↓
  ↑--------↓------------↓
  ↑                     ↓
Error ←-------------------

Implementing States in Design and Code

Design Implementation

When designing components, consider:
  1. Consistent Visual Language: Use consistent visual cues for the same states across different components
  2. Clear State Communication: Ensure each state is visually distinct and communicates its purpose
  3. Transition Design: Define how components transition between states, including timing and animation
  4. Composition: Design states that compose well with other components and in different contexts

Code Implementation

When implementing states in code:
  1. State Naming: Use clear, consistent names for states in class names, data attributes, and variables
  2. Accessibility: Ensure proper ARIA attributes are applied based on the component’s state
  3. Performance: Optimize how state changes trigger rendering updates
  4. Testing: Test all possible state transitions and edge cases
// Example implementation with proper state attributes
function Button({ children, disabled, isLoading, variant = 'primary' }) {
  return (
    <button
      className={`button ${variant}`}
      disabled={disabled || isLoading}
      data-state={isLoading ? 'loading' : disabled ? 'disabled' : 'default'}
      aria-busy={isLoading}
      aria-disabled={disabled || isLoading}
    >
      {isLoading ? <LoadingSpinner /> : children}
    </button>
  );
}

Practical Example: Form Component

Consider a form component with multiple states. Here’s how we might approach its implementation:

States Definition

  • Idle: Initial state, form is ready for input
  • Validating: Checking input validity
  • Submitting: Sending data to server
  • Success: Submission completed successfully
  • Error: Submission failed

Implementation Example

function ContactForm() {
  const [state, setState] = useState('idle');
  const [data, setData] = useState({ name: '', email: '', message: '' });
  const [errors, setErrors] = useState({});

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Transition to validating state
    setState('validating');

    const validationErrors = validateForm(data);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      setState('error');
      return;
    }

    // Transition to submitting state
    setState('submitting');

    try {
      await submitFormData(data);
      setState('success');
    } catch (error) {
      setErrors({ form: error.message });
      setState('error');
    }
  };

  // Render different UI based on current state
  return (
    <div className="form-container" data-state={state}>
      {state === 'success' ? (
        <SuccessMessage />
      ) : (
        <form onSubmit={handleSubmit}>
          {/* Form fields here */}

          {state === 'error' && errors.form && (
            <div className="error-message" role="alert">
              {errors.form}
            </div>
          )}

          <button
            type="submit"
            disabled={state === 'submitting'}
            data-state={state}
          >
            {state === 'submitting' ? 'Submitting...' : 'Submit'}
          </button>
        </form>
      )}
    </div>
  );
}

Best Practices

  1. Minimize State Complexity: Keep state as simple as possible and limit the number of stateful components
  2. Single Source of Truth: Avoid duplicating state across components
  3. Predictable Transitions: Make state transitions clear and predictable
  4. Accessibility First: Ensure states are properly communicated to all users, including those using assistive technologies
  5. Performance Consideration: Be mindful of how state changes affect rendering performance
  6. Test All States: Thoroughly test all states and transitions, including edge cases
  7. Document Everything: Maintain comprehensive documentation of all states and their behaviors
I