Components
Empty state
The screen someone sees on their first day, and the one they see when a filter goes wrong. Treating those as the same screen is the mistake this component exists to prevent.
Three states, not one
"No results" covers all three and helps in none of them. The user's situation is different in each case, and so is the useful next action.
| State | The user's situation | The action |
|---|---|---|
| Nothing yet | First use. They have not done anything wrong; they have not started. | Create the first one. |
| Nothing matched | Data exists but their filter or search excluded it. The system is working. | Clear the filter — and say what it was. |
| Something failed | A request failed. There may well be data; they just cannot see it. | Retry, and say what went wrong. |
Nothing yet
No invoices yet
Invoices you create will appear here, newest first.
Nothing matched
Repeat the filter back. Someone who has forgotten they set a date range three minutes ago cannot fix a result they do not understand.
No invoices match “overdue” in January
There are 14 invoices in other periods. Widen the date range or clear the status filter.
Something failed
Could not load invoices
The request timed out. Your data is safe — this is a problem reaching the server.
When not to use it
- Not for a momentary gap while loading. An empty state that flashes before data arrives reads as a failure. Use a skeleton, or nothing.
- Not with an illustration that says nothing. A drawing of an empty box adds bytes and no information. Halbton ships no illustrations, which is also why the system has no image dependency.
- Not for a section that is optional. If a panel can legitimately be empty forever, hide it.
Writing them
- Title says what is true, not how you feel about it. "No invoices yet", not "Nothing to see here".
- Body says what will change it. One sentence.
- One action. Two competing buttons in an empty state is a decision the user has no information to make.
- No exclamation marks. Nothing here is exciting.
Markup
<div class="ht-empty">
<p class="ht-empty__title">No invoices yet</p>
<p class="ht-empty__body">
Invoices you create will appear here, newest first.
</p>
<button class="ht-btn ht-btn--primary ht-btn--md">
Create an invoice
</button>
</div> Accessibility
- Ordinary content, not a live region — it is on screen when the user arrives, so there is nothing to announce.
- If it replaces a table after a filter changes, move focus to the title or announce it in a live region. Otherwise a screen-reader user is left reading rows that are no longer there.
- The dashed border is decoration. The title carries the meaning, so the state survives with styles off.
- The action is a real button, in the tab order in its natural position.