Designing for States

Updated:

When the main content of a feature can be empty, loading, sparse, overflowing, failed, or color-coded, design every state deliberately, not just the happy path.

A feature is far more than its ideal, filled-in mock. Real data is sometimes missing, still loading, scarce, overwhelming, or wrong, and a view designed only in its perfect state breaks in all the others. Borrowing from the "nine states of design," design for the data your feature will actually hold, not the screenshot you wish it had. The sections below walk the states a view moves through (e.g., loading, empty, sparse, overflowing, and failed), then the done state, and close with one rule that applies across all of them: never let color carry meaning on its own.

Show a deliberate loading state

Render a loading state instead of a blank flash or a layout that jumps when data arrives. Use a q2-loading skeleton whose shape matches the final content, so there is minimal shifting when the data lands. A skeleton shaped like label-value rows tells the user exactly what is coming; a generic spinner tells them nothing.

<q2-loading type="skeleton" shape="label-value" counts="3"></q2-loading>

Design three kinds of empty state

"Empty" is not one state. Three situations look alike but need different content, so handle them distinctly.

First Use

The user has never had data. This is an onboarding moment: lead with an icon or illustration and an emphasized call-to-action, and hide supporting UI (tabs, filters) that has nothing to act on yet.

No savings goals yet

Set a goal and we'll help you track your progress toward it.

Create a goal
<q2-card elevation="0" type="non-clickable">
  <q2-icon type="target" style="--tct-icon-size: 48px;"></q2-icon>
  <h2>No savings goals yet</h2>
  <p>Set a goal and we'll help you track your progress toward it.</p>
  <q2-action-group>
    <q2-btn intent="workflow-primary">Create a goal</q2-btn>
  </q2-action-group>
</q2-card>

No results

A search or filter matched nothing. Don't show the onboarding call-to-action; acknowledge the query and offer a way back to the full set.

No accounts match "savings"

Try a different search, or clear the filter to see all accounts.

Clear filters
<q2-card elevation="0" type="non-clickable">
  <q2-icon type="search" style="--tct-icon-size: 48px;"></q2-icon>
  <h2>No accounts match "savings"</h2>
  <p>Try a different search, or clear the filter to see all accounts.</p>
  <q2-action-group>
    <q2-btn intent="workflow-secondary">Clear filters</q2-btn>
  </q2-action-group>
</q2-card>

Error

The data failed to load. That's an error state (see below), not an empty one. Don't disguise a failure as "nothing here."

Design for one item and for a few

Design the view for a single item and a handful, not just the dozen in your mock. A layout tuned for "some" often looks broken with one item (e.g., an orphaned card stretched full width) or with two, where columns go lopsided. Before you ship, confirm that a single item still looks intentional.

Plan for far more data than you expect

When a list or table outgrows the screen, don't let it run forever. Paginate it, cap it with a "show more", or give the user search and filters to narrow it down. A q2-pagination sitting beneath a long list or table keeps the page bounded.

<q2-pagination total="248" page="1" per-page="10"></q2-pagination>

Make errors recoverable

When something fails, say so clearly and give a way forward. A blank screen or a raw error string leaves the user stuck. Use q2-message type="error" with a short, recoverable next step so they know what happened and what to do about it.

We couldn't load your transactions. Check your connection and try again. Try again
<q2-message type="error">We couldn't load your transactions. Check your connection and try again.</q2-message>
<q2-action-group>
  <q2-btn intent="workflow-secondary">Try again</q2-btn>
</q2-action-group>

Confirm when an action is done

The completion state is the most-skipped one. After a meaningful action, confirm it happened and point to what's next, rather than dropping the user back with no acknowledgement that anything occurred.

Your transfer of $250.00 to Savings is on its way.
<q2-message type="success">Your transfer of $250.00 to Savings is on its way.</q2-message>

Never rely on color alone

Color-blind users can't distinguish red from green, so color is never enough to carry status on its own. Pair every color signal with an icon and a text label (e.g., a trend should carry an arrow icon and a "% higher/lower" label, not color alone). When you need to distinguish a series of values, prefer differences in contrast over differences in hue.

12% lower 4% higher
<q2-grid columns="1" sm-columns="2" gap="comfortable">
  <q2-grid-area>
    <q2-card elevation="1">
      <q2-detail label="Spending vs. last month" stacked size="small">
        <span style="color: var(--const-stoplight-success); display: inline-flex; align-items: center; gap: var(--app-scale-1x);">
          <q2-icon type="arrow-down" style="--tct-icon-size: 16px;"></q2-icon>
          <span>12% lower</span>
        </span>
      </q2-detail>
    </q2-card>
  </q2-grid-area>
  <q2-grid-area>
    <q2-card elevation="1">
      <q2-detail label="Budget used vs. last month" stacked size="small">
        <span style="color: var(--const-stoplight-warning); display: inline-flex; align-items: center; gap: var(--app-scale-1x);">
          <q2-icon type="arrow-up" style="--tct-icon-size: 16px;"></q2-icon>
          <span>4% higher</span>
        </span>
      </q2-detail>
    </q2-card>
  </q2-grid-area>
</q2-grid>
Do design every state the data can be in. Match the loading skeleton to the real content, distinguish first-use from no-results, plan for one item and for too many, and confirm when an action is done.

Don't design only the ideal, filled-in mock. A blank flash, an orphaned single item, an endless list, a silent failure, or color as the only signal are all states left undesigned.