Empty state
The sober block for a list that is empty right now: a filter with no match, a section with nothing yet.
No customers match these filters
Try a different search term, or clear the filters to see everyone again.
import { Button } from '@/components/button'
import { EmptyState } from '@/components/empty-state'
export default function Example() {
return (
<EmptyState
title="No customers match these filters"
description="Try a different search term, or clear the filters to see everyone again."
action={<Button outline>Clear all filters</Button>}
/>
)
}<script setup lang="ts">
import Button from '@/components/button.vue'
import EmptyState from '@/components/empty-state.vue'
</script>
<template>
<EmptyState
title="No customers match these filters"
description="Try a different search term, or clear the filters to see everyone again."
>
<template #action>
<Button outline>Clear all filters</Button>
</template>
</EmptyState>
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import clsx from 'clsx'
import type React from 'react'
export function EmptyState({
title,
description,
icon: Icon,
illustration,
action,
level = 3,
className,
...props
}: {
title: string
description?: string
icon?: React.ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>
illustration?: React.ReactNode
action?: React.ReactNode
level?: 1 | 2 | 3 | 4 | 5 | 6
} & React.ComponentPropsWithoutRef<'div'>) {
let Heading = `h${level}` as const
return (
<div
{...props}
className={clsx(
className,
'flex flex-col items-center justify-center rounded-xl border border-dashed border-zinc-950/10 bg-zinc-50 px-8 py-12 text-center dark:border-white/10 dark:bg-zinc-800'
)}
>
{illustration && (
<div className="flex justify-center [&_svg]:h-40 [&_svg]:w-auto sm:[&_svg]:h-48">{illustration}</div>
)}
{Icon && !illustration && <Icon className="size-10 shrink-0 fill-zinc-400 dark:fill-zinc-500" aria-hidden="true" />}
<Heading
className={clsx(
'text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white',
(illustration || Icon) && 'mt-6'
)}
>
{title}
</Heading>
{description && (
<p className="mx-auto mt-2 max-w-sm text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">{description}</p>
)}
{action && <div className="mt-6">{action}</div>}
</div>
)
}<script setup lang="ts">
import type { Component } from 'vue'
withDefaults(defineProps<{
title: string
description?: string
icon?: Component
level?: 1 | 2 | 3 | 4 | 5 | 6
}>(), { level: 3 })
</script>
<template>
<div
class="flex flex-col items-center justify-center rounded-xl border border-dashed border-zinc-950/10 bg-zinc-50 px-8 py-12 text-center dark:border-white/10 dark:bg-zinc-800"
>
<div v-if="$slots.illustration" class="flex justify-center [&_svg]:h-40 [&_svg]:w-auto sm:[&_svg]:h-48">
<slot name="illustration" />
</div>
<component
:is="icon"
v-if="icon && !$slots.illustration"
class="size-10 shrink-0 fill-zinc-400 dark:fill-zinc-500"
aria-hidden="true"
/>
<component
:is="`h${level}`"
class="text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white"
:class="$slots.illustration || icon ? 'mt-6' : ''"
>
{{ title }}
</component>
<p v-if="description" class="mx-auto mt-2 max-w-sm text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
{{ description }}
</p>
<div v-if="$slots.action" class="mt-6">
<slot name="action" />
</div>
</div>
</template>Component API
| Prop | Default | Description |
|---|---|---|
EmptyStaterenders a <div> element | ||
title | - | What is empty, stated plainly. |
description | - | Why, or what to do about it. |
icon | - | A Heroicon drawn above the title when there is no illustration. |
illustration | - | An SVG rendered above the title, sized to 10rem (12rem from sm). |
level | 3 | Heading level of the title, to fit the outline of the page. |
action | - | A button under the description, "Clear all filters" for example. |
IllustrationFramerenders a <svg> element | ||
| This component does not expose any component-specific props. | ||
| Prop | Default | Description |
|---|---|---|
EmptyStaterenders a <div> element | ||
title | - | What is empty, stated plainly. |
description | - | Why, or what to do about it. |
icon | - | A Heroicon drawn above the title when there is no illustration. |
#illustration | - | Slot for an SVG rendered above the title, sized to 10rem (12rem from sm). |
level | 3 | Heading level of the title, to fit the outline of the page. |
#action | - | Slot for a button under the description, "Clear all filters" for example. |
IllustrationFramerenders a <svg> element | ||
| This component does not expose any component-specific props. | ||
Examples
Basic example
Title, description and an action. This is the state of a table emptied by a filter; a register that was never used gets an Empty page instead:
No customers match these filters
Try a different search term, or clear the filters to see everyone again.
import { Button } from '@/components/button'
import { EmptyState } from '@/components/empty-state'
export default function Example() {
return (
<EmptyState
title="No customers match these filters"
description="Try a different search term, or clear the filters to see everyone again."
action={<Button outline>Clear all filters</Button>}
/>
)
}<script setup lang="ts">
import Button from '@/components/button.vue'
import EmptyState from '@/components/empty-state.vue'
</script>
<template>
<EmptyState
title="No customers match these filters"
description="Try a different search term, or clear the filters to see everyone again."
>
<template #action>
<Button outline>Clear all filters</Button>
</template>
</EmptyState>
</template>With an icon
An icon sits above the title in grey, for a section that has nothing to show yet:
No notes yet
Notes stay internal. The customer never sees them.
<EmptyState
icon={InboxIcon}
title="No notes yet"
description="Notes stay internal. The customer never sees them."
/><template>
<EmptyState
:icon="InboxIcon"
title="No notes yet"
description="Notes stay internal. The customer never sees them."
/>
</template>With an illustration
Draw the illustration inside an IllustrationFrame: it brings the halo, the shadow and the sparkles, you add the paths. Every path reads the accent-* tokens:
No reviews pending
Payments flagged by Shield land here for a decision.
import { EmptyState } from '@/components/empty-state'
import { IllustrationFrame } from '@/components/illustration-frame'
function ShieldIllustration(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<IllustrationFrame {...props}>
<path className="fill-accent-600" d="M132 74l58 20v56c0 40-24 68-58 82-34-14-58-42-58-82V94z" />
<path className="stroke-white dark:stroke-zinc-800" d="M108 148l16 16 32-34" strokeWidth="10" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</IllustrationFrame>
)
}
export default function Example() {
return (
<EmptyState
illustration={<ShieldIllustration />}
title="No reviews pending"
description="Payments flagged by Shield land here for a decision."
/>
)
}<script setup lang="ts">
import EmptyState from '@/components/empty-state.vue'
import ShieldIllustration from './shield-illustration.vue'
</script>
<template>
<EmptyState title="No reviews pending" description="Payments flagged by Shield land here for a decision.">
<template #illustration>
<ShieldIllustration />
</template>
</EmptyState>
</template>Page accent
Set one of the accent-* utilities on a page root and every illustration and accented button inside it follows. Without it, the accent falls back to blue. The tokens are declared once in your stylesheet, shown on the Empty page:
No reviews pending
<div className="accent-violet">
<EmptyState illustration={<ShieldIllustration />} title="No reviews pending" />
</div><template>
<div class="accent-violet">
<EmptyState title="No reviews pending">
<template #illustration>
<ShieldIllustration />
</template>
</EmptyState>
</div>
</template>Search results
The same block works inside a panel or a popover, with a search icon:
No results for “acme”
<EmptyState icon={MagnifyingGlassIcon} title="No results for “acme”" /><EmptyState :icon="MagnifyingGlassIcon" title="No results for “acme”" />