Empty
What a region shows when it loaded and has nothing: why, and what to do next.
In Dense data
See it live
npm run storybookWhen to use it
In Dense data: An empty table says what to do next.
- Use it when: An empty list, a search with no results, or a first visit with no items yet.
- Not when: Still loading (skeleton or spinner), or a load that failed (alert).
Instead:
Variants
What each variant is for in any design system, then what Dense data decided and why. Each sample is the live story with that variant set.
defaultvariantNot used in Dense dataWhat it is for
The picture shown as it is: an image or illustration.
In Dense data
No pictures: an empty table needs the reason and the one action that fills it.
iconvariantUsed in Dense dataWhat it is for
The picture as an icon in a small tile.
In Dense data
An outline icon in a small tile beside the reason and the action.
How Dense data uses it
Why it works this way
Empty looks the way it does because of 4 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Empty, and what would move if it changed.
content.q11The answer
Most of the screen is numbers and records.
2So Empty
- –does not use default: No pictures: an empty table needs the reason and the one action that fills it.
Badge: neutral Badge: info Badge: success Badge: warning Badge: danger Sidebar: default Sidebar: outline Item: default Item: outline Toggle: default Toggle: outline3If it changed
and 44 answers that follow from it: A lot, Scan and compare, People keep their place, and 41 more.
voice.lands1The answer
Errors and empty tables are where words decide the next action.
2So Empty
- –does not use default: No pictures: an empty table needs the reason and the one action that fills it.
Toast: danger3If it changed
No other answer follows from it.
iconography.roles1The answer
Icons mark row actions and status.
2So Empty
- ✓uses icon: An outline icon in a small tile beside the reason and the action.
Button: icon Input group: inline-start3If it changed
No other answer follows from it.
iconography.definition1The answer
Outline icons stay light next to dense text.
2So Empty
- ✓uses icon: An outline icon in a small tile beside the reason and the action.
3If it changed
No other answer follows from it.
Words
- Lead with the object, then the state: "Alert 2041 contained."
- Use numbers, not words for numbers.
- One verb per button. No "please", no exclamation marks.
- Use: Investigate, Filter, Assign, Escalated, Resolved. Avoid: Oops, Awesome, Please, Simply.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Cancelcontent.action.destroy: Deletecontent.action.retry: Retrycontent.action.empty-action: Change filters
See Voice and tone for the whole voice.
Use it in code
npx open-ds add empty
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Empty reads 15 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Empty's tokens to change Empty alone.
Dense data decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Empty | ||
| Gap | component.empty.gapspace.gap.sectionvar(--empty-gap) | space.24:1.5rem |
| Corner | component.empty.radiusradius.containervar(--empty-radius) | radius.4:0.25rem |
| Padding | component.empty.paddingcomponent-spacing-map.x6var(--empty-padding) | space.24:1.5rem |
| Padding 2 | component.empty.padding-2component-spacing-map.x12var(--empty-padding-2) | space.48:3rem |
| Header | ||
| Gap | component.empty.header.gapspace.gap.defaultvar(--empty-header-gap) | space.8:0.5rem |
| Icon | ||
| Corner | component.empty.icon.radiusradius.containervar(--empty-icon-radius) | radius.4:0.25rem |
| Fill Changes with theme | component.empty.icon.backgroundcolor.surface.sunkenvar(--empty-icon-background) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Text Changes with theme | component.empty.icon.labelcolor.text.primaryvar(--empty-icon-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Title | ||
| Text size | component.empty.title.font-sizetype.heading.font-sizevar(--empty-title-font-size) | font.size.16:1rem |
| Weight | component.empty.title.font-weighttype.label.font-weightvar(--empty-title-font-weight) | font.weight.500:500 |
| Description | ||
| Text size | component.empty.description.font-sizetype.body.font-sizevar(--empty-description-font-size) | font.size.14:0.875rem |
| Text Changes with theme | component.empty.description.labelcolor.text.mutedvar(--empty-description-label) | Lightslate.light.600:#535D6DDarkslate.dark.400:#969FAD |
| Label 2 Changes with theme | component.empty.description.label-2color.action.primaryvar(--empty-description-label-2) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Content | ||
| Gap | component.empty.content.gapspace.gap.groupvar(--empty-content-gap) | space.16:1rem |
| Text size | component.empty.content.font-sizetype.body.font-sizevar(--empty-content-font-size) | font.size.14:0.875rem |