Feedback States

Unified pattern for empty, loading, and error states. State-driven feedback with semantic HTML and accessible output elements.

Overview

Feedback states communicate container status to users. Instead of separate components, this pattern uses a single data-attribute system to toggle between empty, loading, and error states.

Key concepts:

  • data-state="empty|loading|error" on the container controls visibility
  • output[data-empty|loading|error] elements contain state-specific feedback
  • .content class marks the populated content (table, ul, ol, dl, articles)
  • data-feedback="message|skeleton" controls presentation style

The Pattern

Put data-state directly on semantic containers (section, article). Use <output> elements with data attributes for each feedback type.

/* Default: feedback outputs hidden */ output[data-empty], output[data-loading], output[data-error] { display: none; } /* When state is set: hide content, show matching feedback */ [data-state="empty"] > .content, [data-state="loading"] > .content, [data-state="error"] > .content { display: none; } [data-state="empty"] > [data-empty], [data-state="loading"] > [data-loading], [data-state="error"] > [data-error] { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--size-xl); } /* Feedback presentation variants */ [data-feedback="skeleton"] { gap: var(--size-s); } [data-feedback="message"] { gap: var(--size-m); }

Data Attributes

Attribute Applied To Purpose
data-state="empty|loading|error" Container Current state of the container
data-empty Output Feedback shown when state is empty
data-loading Output Feedback shown when state is loading
data-error Output Feedback shown when state is error
data-feedback="message|skeleton" Output Presentation style (text vs visual)

Basic Structure

A container with a single empty state feedback output.

<section class="messages" data-state="empty"> <h2>Messages</h2> <!-- Populated content --> <ul class="content"> <li>Message 1</li> </ul> <!-- Feedback for empty state --> <output data-empty data-feedback="message" role="status"> <icon-wc name="inbox"></icon-wc> <h3>No messages</h3> <p>Messages will appear here.</p> </output> </section>

Multiple States

A single container can have feedback for multiple states. Only the matching feedback is visible based on data-state.

<section class="products" data-state="loading"> <h2>Products</h2> <table class="content">...</table> <!-- Loading feedback (skeleton style) --> <output data-loading data-feedback="skeleton" role="status" aria-busy="true"> <span class="skeleton-line"></span> <span class="skeleton-line"></span> </output> <!-- Empty feedback --> <output data-empty data-feedback="message" role="status"> <icon-wc name="package"></icon-wc> <h3>No products</h3> </output> <!-- Error feedback --> <output data-error data-feedback="message" role="alert"> <icon-wc name="alert-circle"></icon-wc> <h3>Failed to load</h3> </output> </section>

Table Example

A table with all three feedback states. Use the buttons to toggle between states.

List Examples

Unordered list (notifications) and ordered list (leaderboard) with feedback states. The notifications use a message-style loading state, while the leaderboard uses skeleton lines.

Description List Example

A product specifications panel using a description list (<dl>) with skeleton loading feedback.

Articles Example

Blog posts as a collection of articles with feedback states for empty, loading, and error conditions.

Feedback Types

Use data-feedback to control the presentation style of feedback outputs.

Type Use For Contents
message Empty and error states, short loading messages Icon, heading, description, optional action button
skeleton Loading states showing content structure Animated placeholder lines matching expected content

JavaScript Integration

The pattern works CSS-only with server-rendered data-state. For dynamic content, toggle the attribute based on data fetching status.

// Toggle state based on data function updateState(container, state) { if (state === 'populated') { container.removeAttribute('data-state'); } else { container.dataset.state = state; } } // Example: after fetching data fetchData().then(items => { if (items.length === 0) { updateState(container, 'empty'); } else { updateState(container, 'populated'); renderItems(items); } }).catch(error => { updateState(container, 'error'); });

Accessibility

  • <output> element: Semantic element for content that is the result of a user action or calculation. Screen readers announce it naturally.
  • role="status": Live region for empty and loading states. Changes are announced politely.
  • role="alert": Live region for error states. Changes are announced immediately.
  • aria-busy="true": Indicates loading state to assistive technologies.
State Role Additional Attributes
Empty role="status" None
Loading role="status" aria-busy="true"
Error role="alert" None

Icon Options

Choose icons that represent the state and content type:

  • Empty: inbox, file-text, users, folder, bell-off, trophy
  • Loading: loader (with data-animate="spin")
  • Error: alert-circle, alert-triangle
  • Retry action: refresh-cw

Usage Notes

  • No wrapper divs: Apply data-state directly to semantic containers like <section> or <article>
  • Content class: Always mark your populated content with .content class
  • Multiple outputs: Include all relevant feedback outputs in the container; CSS shows only the matching one
  • Progressive enhancement: Works CSS-only; JavaScript enhances by toggling data-state
  • Skeleton matching: For skeleton loading, match the number and width of lines to your expected content

Related

Empty States

Detailed empty state examples

Skeleton

Skeleton animation details

Error Pages

Full-page error states (404, 500)

Output Element

Semantic output element reference