State Diagrams for UI Design

Model complex user interface behavior with finite state machines and Mermaid.js

Every button, form, and modal in your application has a lifecycle. A submit button can be idle, loading, disabled, or showing an error. A data table can be empty, loading, populated, or in a failure state. When developers skip modeling these states explicitly, the result is often a tangle of boolean flags -- isLoading && !hasError && data.length > 0 -- scattered across components. State diagrams offer a better way.

What Is a State Diagram?

A state diagram is a visual representation of a finite state machine (FSM). It shows every possible state a system can be in, along with the transitions (events or actions) that move it from one state to another. In formal terms, an FSM has a finite set of states, a starting state, a set of accepted inputs, and a transition function that maps a current state plus an input to a next state.

For UI work, "system" is typically a single component or screen, "states" are the visual modes the user can see, and "transitions" are user actions or asynchronous events like API responses. The value of drawing these out is clarity: the diagram forces you to account for every combination and every edge case before you write a single line of code.

Why State Diagrams Matter for UI

Complex user interfaces fail in predictable ways. A modal opens behind another modal. A form submits twice because the loading state was not enforced. An error message persists after the user corrects the input. Each of these bugs stems from an implicit, undocumented state machine hiding in the code.

State diagrams address these problems by making the invisible visible:

  • Exhaustive coverage -- You enumerate every state, so "impossible" states become visible before they ship.
  • Shared vocabulary -- Designers, QA, and developers can point at the same diagram during a review.
  • Direct translation to code -- Libraries like XState and Robot let you implement FSMs directly from a diagram.
  • Reduced boolean complexity -- Instead of tracking five booleans, you track one current state.

Mermaid.js stateDiagram Syntax

Mermaid provides a dedicated stateDiagram-v2 block for drawing state diagrams. The syntax is concise: you define states, connect them with arrows, and label the transitions.

stateDiagram-v2 [*] --> Idle Idle --> Loading : fetch Loading --> Success : resolve Loading --> Error : reject Error --> Loading : retry Success --> [*]

The [*] symbol represents the start and end pseudo-states. Arrows use -->, and the text after the colon is the transition label. You can nest states inside composite states, add notes, and apply styling -- all within the same text block.

Example 1: Data Fetching States

The most common UI pattern is fetching data from an API. Every component that loads remote data cycles through the same states. Here is the full picture:

stateDiagram-v2 [*] --> Idle Idle --> Loading : component mounts Loading --> Empty : response is empty Loading --> Populated : response has data Loading --> Error : request fails Empty --> Loading : refresh Populated --> Loading : refresh Error --> Loading : retry Populated --> [*]

This diagram accounts for the empty state that many implementations forget. Without it, users see a blank screen with no explanation. Mapping these states first ensures your UI covers the empty case with a helpful message or illustration.

Translating to React

With this diagram in hand, the component logic becomes a straightforward switch:

// Derived from the state diagram above switch (state) { case 'idle': return null; case 'loading': return <Spinner />; case 'empty': return <EmptyState onRefresh={refetch} />; case 'populated': return <DataTable rows={data} />; case 'error': return <ErrorBanner onRetry={refetch} />; }

Example 2: Form Validation States

Forms are surprisingly stateful. A single input field moves through pristine, touched, validating, valid, invalid, and submitting states. Here is a state diagram for a complete form lifecycle:

stateDiagram-v2 [*] --> Pristine Pristine --> Touched : user types Touched --> Validating : blur / submit Validating --> Valid : passes rules Validating --> Invalid : fails rules Invalid --> Touched : user edits Valid --> Submitting : submit Submitting --> Success : 200 OK Submitting --> ServerError : 4xx / 5xx ServerError --> Touched : user edits Success --> [*]

Notice that Submitting is its own state, separate from validation. This prevents the common bug where a user clicks "Submit" twice because the button was not disabled during the request. The diagram makes it obvious: once you enter Submitting, the only exits are Success or ServerError.

Example 3: Multi-Step Wizard Flow

Modal wizards and multi-step forms involve navigation between steps plus an overall submission flow. Composite states in Mermaid let you nest the step logic inside a parent state:

stateDiagram-v2 [*] --> Closed Closed --> Open : click "New Project" state Open { [*] --> Step1 Step1 --> Step2 : next Step2 --> Step1 : back Step2 --> Step3 : next Step3 --> Step2 : back Step3 --> Confirming : submit Confirming --> Complete : confirmed Confirming --> Step3 : cancel } Open --> Closed : dismiss / complete Closed --> [*]

Composite states keep the diagram readable. The outer level shows Open vs. Closed. The inner level details the wizard steps. A developer reading this knows immediately that dismissing the modal from any inner step returns the system to Closed.

Example 4: Authentication Guard

Authentication state affects almost every screen in an application. Modeling it as a state machine prevents the common bug where a user sees a flash of protected content before being redirected to login:

stateDiagram-v2 [*] --> Checking Checking --> Authenticated : token valid Checking --> Unauthenticated : no token / expired Unauthenticated --> Authenticating : login attempt Authenticating --> Authenticated : success Authenticating --> Unauthenticated : failure Authenticated --> Unauthenticated : logout / token expired Authenticated --> [*]

This diagram highlights the Checking state -- the brief moment on page load when the app has not yet determined whether the user is logged in. Rendering a splash screen or skeleton during Checking prevents the content flash.

Best Practices for UI State Diagrams

  • Start from the user's perspective. Name states after what the user sees (Empty, Loading, Error), not after internal variables.
  • Make impossible states impossible. If two states cannot coexist, they should be separate branches, not parallel booleans.
  • Label every transition. Unlabeled arrows are ambiguous. The label is the event or action that causes the change.
  • Keep it to one concern. A single diagram should cover one component or one workflow. If it grows past ten states, consider splitting it.
  • Iterate alongside design. Create the diagram during design review, not after implementation. It is far cheaper to fix a missing state in a diagram than in shipped code.

Tip: You can paste any of these examples directly into the SimpleMermaid editor to see them rendered live. Edit the states and transitions to match your own component and share the link with your team.

From Diagram to Implementation

State diagrams are not just documentation -- they are a specification. Libraries like XState accept machine definitions that mirror Mermaid syntax almost one-to-one. Even without a library, a well-drawn state diagram gives you the exact switch-case or reducer logic your component needs.

The next time you reach for a second boolean to track UI state, pause and draw the state diagram first. You will likely discover a missing state, an unhandled transition, or a simpler way to organize the logic. The five minutes spent on the diagram will save hours of debugging later.

Try It Yourself

Paste any of these state diagrams into the SimpleMermaid editor and customize them for your project.

Open the Editor