UI/UX Atlas
Interaction Design Intermediate

System States: Loading, Skeleton, Empty, Error & Success

Designing every possible state an interface can reach — loading, empty, error, and success — is what separates polished products from ones that feel broken at the edges.

10 min read

Interactive example · System states

Skeleton screens preserve layout and feel faster than a spinner for content-heavy views.

The full lesson

Every UI has at least five distinct states. But most design time goes to exactly one: the ideal state where data is loaded and everything worked. The other four — loading, skeleton, empty, error, and success — are where trust is actually built or lost.

A user who hits an unhandled empty state with no recovery path, or watches a generic spinner for eight seconds on a slow connection, forms a negative impression that sticks long after the content finally loads. Thinking in states is not a finishing step. It belongs in the interaction model from the very first wireframe.

The State Machine Mindset

Before designing individual states, define the complete set of states your component or screen can be in. This is called state machine thinking: every component moves through a predictable set of conditions, and the interface must have a designed response for each one.

A typical data-driven view has at minimum seven states:

  • Idle — nothing has been requested yet (often the pre-interaction state)
  • Loading — a request is in flight
  • Skeleton — a structural placeholder is shown while content resolves
  • Populated — data is available and rendered
  • Empty — the request succeeded but returned zero results
  • Error — the request failed, or a user action produced a recoverable failure
  • Success — a user-initiated action completed successfully

Designing all seven explicitly prevents the most common production failure: an unhandled state that surfaces as a blank screen, a broken layout, or a cryptic browser-level error message.

Loading States: Spinners Done Right

The spinner is not obsolete — it is simply over-applied. Use a spinner for operations that block the interface for a short, unpredictable amount of time: submitting a form, uploading a file, running a search query. It tells the user: “the system is working; please wait.”

Spinners become a problem when:

  • The wait exceeds roughly four seconds without any progress feedback
  • A full-screen spinner covers the entire viewport
  • Multiple spinners appear at the same time in different parts of the UI

For waits longer than four seconds, switch to a progress indicator — a percentage, a step count, or an estimated time remaining. Users who can see real progress are more tolerant of delays and less likely to abandon the task.

Motion and Accessibility

Spinners and loading indicators must respect the prefers-reduced-motion media query. A spinning SVG that cannot be paused violates WCAG 2.2 Success Criterion 2.3.3 (Animation from Interactions, AAA) and causes genuine discomfort for users with vestibular disorders. Use a CSS @media (prefers-reduced-motion: reduce) rule to either stop the rotation or replace the animation with a static “Loading…” label.

Loading indicators must also communicate their state to assistive technology. An aria-live="polite" region that announces “Loading content” when the request starts and “Content loaded” when it finishes covers screen reader users who cannot see the visual indicator.

Do

  • Use spinners for short, bounded waits (under 4 seconds) where the final structure is unknown.
  • Use skeleton screens for content-heavy layouts where the structure is predictable.
  • Add aria-live announcements so screen reader users hear state transitions.
  • Respect prefers-reduced-motion — offer a static alternative to any animated indicator.
  • Show a progress bar with a percentage or step count for operations lasting more than 4 seconds.

Don't

  • Use a full-screen spinner for every loading state regardless of duration or content type.
  • Display multiple simultaneous spinners in unrelated parts of the page.
  • Leave old content visible and stale while new data loads silently in the background.
  • Animate with width, height, top, or left — use transform and opacity only, which stay on the compositor thread and avoid layout thrash.
  • Loop a decorative spinner animation that cannot be stopped — this is both a usability failure and a potential WCAG violation.

Skeleton Screens: Structural Honesty

A skeleton screen is a placeholder layout that mirrors the structure of the content being loaded. Grey bars stand in for text. Grey rectangles stand in for images. Skeleton screens are now the preferred pattern for content-heavy layouts — feeds, dashboards, article pages, search results — because they dramatically reduce perceived wait time.

The psychology behind this is well-established. Skeletons preview the coming structure, letting users start building their mental model before the content arrives. A spinner gives no structural information at all. A skeleton gives a lot.

When to Use Skeletons vs. Spinners

ScenarioPrefer
Feed or list of cards with predictable layoutSkeleton
Dashboard with multiple data panelsSkeleton
Full-page initial load of article contentSkeleton
Short form submission (under 2–3 seconds)Spinner
File upload with progressProgress bar
Inline button action (save, like, follow)Inline spinner on button
Unpredictable layout (modal, dynamic content)Spinner

Skeleton Design Principles

Skeletons should closely match the final layout in proportions and element count. A skeleton showing three card placeholders that resolves into twelve cards creates a jarring layout shift. This is both a usability problem and a Core Web Vitals problem — Cumulative Layout Shift (CLS) is a Google ranking signal. Match skeleton height to the expected content height as closely as possible.

The shimmer animation — a diagonal light-sweep that plays across the skeleton — is a widely understood signal for “this is loading, not broken.” Implement it with a CSS background-position animation on a gradient, using transform or opacity where possible. Respect prefers-reduced-motion by disabling the shimmer and showing a static muted skeleton instead.

Skeletons must not be exposed to assistive technology as real content. Wrap skeleton containers in aria-hidden="true" and pair them with an aria-live region that announces load progress and completion.

Empty States: The Underdesigned State

Empty states are perhaps the most neglected of the five. A component that shows zero results — because no data exists yet, no search matched, or no action has been taken — is one of the highest-leverage moments in onboarding. Most teams ship it as a plain “No results” label.

An effective empty state answers three questions:

  1. Why is it empty? (Was this intentional, or did something go wrong?)
  2. What can the user do next? (A clear recovery or next action)
  3. What will this look like when populated? (Helps the user understand the value of the space)

Types of Empty States

  • First-use empty. The user has never taken the action that populates this view. This is a marketing and onboarding moment. Show what the feature does and give a clear call to action — “Create your first project” — with an illustration or example.
  • User-cleared empty. The user archived, deleted, or completed everything. Affirm their action positively: “All caught up!” or “Inbox zero.” This is a small success moment.
  • No-results empty. A search or filter returned nothing. Explain the context (“No results for ‘darkmode font’”), offer recovery paths (clear filter, search tips, browse alternatives), and never leave the user stranded.
  • Error-caused empty. Content should be here but failed to load. Distinguish this clearly from a genuine empty state, or users will assume the feature is simply unused.

Error States: Recovery Over Blame

Error states exist at two levels: system errors (network failure, server timeout, permission denied) and user-input errors (invalid form values, constraint violations). Both require the same fundamental approach: prioritize recovery over blame, and be specific.

System Errors

A network failure that renders an entire screen blank — no retry option, no explanation — is the worst possible error state. Modern best practice:

  • Show inline, contextual errors rather than full-page overlays wherever possible. A single card that failed to load should show an error within the card’s footprint, not crash the whole page.
  • Provide a retry action. A “Try again” button with an aria-label that names what is being retried (“Retry loading your notifications”) is the minimum viable recovery path.
  • Be honest about the cause without being technical. “We couldn’t connect to our servers — check your internet connection and try again” is better than “Error 503” and better than “Something went wrong,” which tells the user nothing actionable.
  • Preserve any user input across error transitions. A form that clears all fields on a server error is a usability catastrophe.

Input Errors

WCAG 2.2 Success Criterion 3.3.1 (Error Identification) requires that input errors be identified in text and described so the user can correct them. Best practice goes further:

  • Validate on blur (when the user leaves a field), not only on submit. This gives immediate feedback at the point of input without interrupting flow.
  • Use aria-describedby to link error messages to their input. This ensures screen readers announce the error message when the field is focused.
  • Write error messages as specific instructions, not condemnations. “Enter a date after today’s date” beats “Invalid date” by every usability metric.
  • Use color as a redundant signal (red border + icon + text). Never rely on color alone — color-blind users must not miss error states.

Error message placement matters: put it inline, directly below or beside the field it refers to. Do not use a floating toast or banner that may be dismissed or missed.

Success States: Close the Loop

Success feedback is the most neglected of the five states in terms of design investment — and one of the most impactful for trust and comprehension. When a user completes an action, the interface must confirm that the action was received and describe the outcome.

Transient vs. Persistent Success

  • Transient success (a toast notification or an inline confirmation that fades away) is appropriate for actions that do not change the page’s primary content — sending a message, saving a draft, copying a link. Keep these brief, auto-dismissible after 4–7 seconds, and dismissible via keyboard (Escape key) for accessibility. Position them consistently (typically bottom-left or top-right) and never in a spot where they obscure primary actions.
  • Persistent success is appropriate when the UI state itself reflects the outcome — a button that changes from “Follow” to “Following,” a toggle that shows its new state, a form that collapses and shows a confirmation summary. In these cases, the state of the element is the success message.

Inline Button Feedback

For action buttons, the interaction model should be: loading state (a spinner briefly replaces the label) followed by success state (a checkmark or updated label) that either persists or fades back to the default. This closed-loop pattern — request in flight, then request confirmed — is the minimum standard for any action whose result is not immediately obvious from the page content.

Avoid navigating away from the page immediately after a success state the user has not had time to read. On form submissions in particular: if you redirect, add a brief delay (300–500 ms) so the user sees the success state before the page changes.

Tokens and Theming for System States

System states should be represented in your design token system, not as one-off color values. A three-tier token architecture keeps them consistent across components and themes.

Primitive layer — raw OKLCH values (perceptually uniform, so they look right in both light and dark themes):

  • --color-red-60: oklch(60% 0.19 25) (error hue, light theme)
  • --color-green-55: oklch(55% 0.16 145) (success hue, light theme)

Semantic layer — the tokens that components actually consume:

  • --color-status-error: var(--color-red-60)
  • --color-status-success: var(--color-green-55)
  • --color-status-loading: var(--color-neutral-40)

Component layer — tokens scoped to specific UI elements:

  • --input-border-error: var(--color-status-error)
  • --toast-bg-success: var(--color-status-success)

This structure means a theme swap (including dark mode) only requires changing the semantic layer values. Every component that consumes a semantic token updates automatically. It also means your status colors meet WCAG 2.2 AA contrast requirements in both themes without per-component overrides, because the OKLCH primitive was chosen with the right lightness value for each theme layer.

Accessibility Checklist for All Five States

Every system state must be independently accessible — not just the ideal populated state:

  • Loading/Skeleton: aria-busy="true" on the loading region; an aria-live="polite" region announces transitions; aria-hidden="true" on visual skeleton elements.
  • Empty: Empty state content is in the normal document flow, not hidden. CTAs in empty states are standard focusable elements with meaningful labels.
  • Error (system): The error message is in the DOM, not conveyed only by color or icon. The retry action is keyboard-focusable. WCAG 2.2 SC 4.1.3 (Status Messages) requires status messages be programmatically determinable without focus — use role="alert" or aria-live="assertive" for critical errors.
  • Error (input): aria-invalid="true" on the affected input; aria-describedby linking to the error message text; error text is visible and persistent (not a tooltip that disappears on blur).
  • Success: Transient toasts use role="status" (polite) or role="alert" (assertive) based on urgency. Persistent state changes announce their new value to assistive technology via aria-label updates or live region content.