Empty state
An illustrated placeholder for a list with nothing in it.
vue
<script setup lang="ts">
import EmptyState from '@shared/components/feedback/EmptyState.vue'
import { ref } from 'vue'
const created = ref('')
</script>
<template>
<div class="space-y-3">
<EmptyState
variant="no-data"
title="No projects yet"
description="Projects group the work you bill for. Create the first one to get started."
action-label="New project"
:action-handler="() => (created = 'Opened the new project form')"
/>
<p v-if="created" class="text-center text-sm text-muted-foreground">
{{ created }}
</p>
</div>
</template>Usage
title is the only required prop. description says what to do next, and actionLabel with actionHandler puts a button under it.
The illustrations are inline SVG components, drawn in muted-foreground at 60% so they read as a placeholder rather than as content. Nothing is fetched.
DataTable renders this component for its own empty state, with the no-data variant. See DataTable.
Examples
Variants
variant is no-data, search, empty-inbox, error or success. Each has its own illustration, and the words are yours.
vue
<script setup lang="ts">
import EmptyState from '@shared/components/feedback/EmptyState.vue'
import { Button } from '@shared/components/ui/button'
import { ref } from 'vue'
const VARIANTS = ['no-data', 'search', 'empty-inbox', 'error', 'success'] as const
const variant = ref<typeof VARIANTS[number]>('no-data')
const COPY: Record<typeof VARIANTS[number], { title: string, description: string }> = {
'no-data': { title: 'No projects yet', description: 'Create the first one to get started.' },
'search': { title: 'No results for "warehouse"', description: 'Check the spelling, or clear the filters.' },
'empty-inbox': { title: 'Inbox zero', description: 'Nothing is waiting on you right now.' },
'error': { title: 'We could not load this', description: 'The request timed out. Try again in a moment.' },
'success': { title: 'All caught up', description: 'Every ticket in this queue has been answered.' },
}
</script>
<template>
<div class="space-y-3">
<div class="flex flex-wrap justify-center gap-2">
<Button
v-for="v in VARIANTS"
:key="v"
size="sm"
:variant="variant === v ? 'default' : 'outline'"
@click="variant = v"
>
{{ v }}
</Button>
</div>
<EmptyState :variant="variant" :title="COPY[variant].title" :description="COPY[variant].description" />
</div>
</template>Title only
vue
<script setup lang="ts">
import EmptyState from '@shared/components/feedback/EmptyState.vue'
</script>
<template>
<EmptyState variant="empty-inbox" title="Nothing here yet" />
</template>API
Source: src/shared/components/feedback/EmptyState.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'search' | 'no-data' | 'error' | 'empty-inbox' | 'success' | 'no-data' | Picks the illustration. |
title | string | required | The heading. |
description | string | — | One or two lines under it, capped at a readable width. |
actionLabel | string | — | The button's text. No label, no button. |
actionHandler | () => void | — | Called on press. |
Emits
None.
Slots
None.
Accessibility
- The title is an
<h3>, so the state takes its place in the page's outline rather than being a loose line of text. - The illustration carries no text and adds nothing a screen reader needs, and the title beside it says what the state is.
- The description is capped at a readable measure rather than running the full width of a wide table.