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.
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>
"Empty" is not one state. Three situations look alike but need different content, so handle them distinctly.
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.
Set a goal and we'll help you track your progress toward it.
<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>
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.
Try a different search, or clear the filter to see all accounts.
<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>
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 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.
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>
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.
<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>
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.
<q2-message type="success">Your transfer of $250.00 to Savings is on its way.</q2-message>
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.
<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>