Empty State
Shows that there is nothing here yet, and what to do about it.
- v1.0.0
- SSR
- Zoneless
- 0.8 kB gzip
No projects yet
You haven't created a project yet. Start by creating your first one.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/ui/empty/empty';
/** No projects yet, with actions to create one. */
@Component({
selector: 'example-empty-demo',
imports: [
Button,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
NdIcon,
],
template: `
<div ndEmpty class="border">
<div ndEmptyHeader>
<div ndEmptyMedia variant="icon"><svg ndIcon="folder-open"></svg></div>
<h2 ndEmptyTitle>No projects yet</h2>
<p ndEmptyDescription>
You haven't created a project yet. Start by creating your first one.
</p>
</div>
<div ndEmptyContent>
<div class="flex gap-2">
<button ndButton>Create project</button>
<button ndButton variant="outline">Import project</button>
</div>
<a ndButton variant="link" href="#docs" size="sm"
>Read about projects</a
>
</div>
</div>
`,
})
export class EmptyDemoExample {}
An empty state fills the space where content would be when there's none yet: no projects, no results, no messages. It says what's missing and what to do about it, with an optional icon or image and actions.
Installation
npx @ndop/cli add emptypnpm dlx @ndop/cli add emptyyarn dlx @ndop/cli add emptybunx @ndop/cli add emptyInstall the dependencies
npm install @ndop/corepnpm add @ndop/coreyarn add @ndop/corebun add @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/empty/empty.tsimport { computed, Directive, input } from '@angular/core'; import { cn, variants, type VariantOf } from '@/lib/utils'; /** * An empty state: shown where content would be, when there's none yet, * with what to do about it. * * @example * ```html * <div ndEmpty> * <div ndEmptyHeader> * <div ndEmptyMedia variant="icon"><svg ndIcon="inbox"></svg></div> * <h3 ndEmptyTitle>No messages</h3> * <p ndEmptyDescription>New messages show up here.</p> * </div> * <div ndEmptyContent><button ndButton>Compose</button></div> * </div> * ``` */ @Directive({ selector: '[ndEmpty]', host: { 'data-slot': 'empty', '[class]': 'classes()' }, }) export class Empty { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12', this.class(), ), ); } /** * An empty state's header: its media, title and description. * * @example * ```html * <div ndEmptyHeader>…</div> * ``` */ @Directive({ selector: '[ndEmptyHeader]', host: { 'data-slot': 'empty-header', '[class]': 'classes()' }, }) export class EmptyHeader { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('flex max-w-sm flex-col items-center gap-2 text-center', this.class()), ); } /** * Class table for {@link EmptyMedia}. */ export const emptyMediaVariants = variants({ base: 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0', variants: { variant: { default: 'bg-transparent', icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6", }, }, defaults: { variant: 'default' }, }); /** Visual style of an {@link EmptyMedia}. */ export type EmptyMediaVariant = VariantOf<typeof emptyMediaVariants, 'variant'>; /** * An image or icon above an empty state's title; `variant="icon"` puts an * icon on a muted tile. * * @example * ```html * <div ndEmptyMedia variant="icon"><svg ndIcon="inbox"></svg></div> * ``` */ @Directive({ selector: '[ndEmptyMedia]', host: { 'data-slot': 'empty-icon', '[attr.data-variant]': 'variant()', '[class]': 'classes()', }, }) export class EmptyMedia { /** Visual style. */ readonly variant = input<EmptyMediaVariant>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn(emptyMediaVariants({ variant: this.variant() }), this.class()), ); } /** * An empty state's title. Put it on the heading element that fits the * page's outline. * * @example * ```html * <h3 ndEmptyTitle>No messages</h3> * ``` */ @Directive({ selector: '[ndEmptyTitle]', host: { 'data-slot': 'empty-title', '[class]': 'classes()' }, }) export class EmptyTitle { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('text-lg font-medium tracking-tight', this.class()), ); } /** * What an empty state means, and what to do; links in it are underlined. * * @example * ```html * <p ndEmptyDescription>New messages show up here.</p> * ``` */ @Directive({ selector: '[ndEmptyDescription]', host: { 'data-slot': 'empty-description', '[class]': 'classes()' }, }) export class EmptyDescription { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary', this.class(), ), ); } /** * An empty state's actions, under its header. * * @example * ```html * <div ndEmptyContent><button ndButton>Compose</button></div> * ``` */ @Directive({ selector: '[ndEmptyContent]', host: { 'data-slot': 'empty-content', '[class]': 'classes()' }, }) export class EmptyContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Button } from '@/ui/button/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/ui/empty/empty';<div ndEmpty class="border">
<div ndEmptyHeader>
<div ndEmptyMedia variant="icon"><svg ndIcon="folder-open"></svg></div>
<h2 ndEmptyTitle>No projects yet</h2>
<p ndEmptyDescription>
You haven't created a project yet. Start by creating your first one.
</p>
</div>
<div ndEmptyContent>
<div class="flex gap-2">
<button ndButton>Create project</button>
<button ndButton variant="outline">Import project</button>
</div>
<a ndButton variant="link" href="#docs" size="sm"
>Read about projects</a
>
</div>
</div>Examples
Empty State search
No search results, with a way to clear the search.
No results
Nothing matches “quarterly report 2019”. Check the spelling or try other words.
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/ui/empty/empty';
/**
* No search results, which says what was searched and offers to clear
* the search.
*/
@Component({
selector: 'example-empty-search',
imports: [
Button,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
NdIcon,
],
template: `
<div ndEmpty role="status">
<div ndEmptyHeader>
<div ndEmptyMedia variant="icon"><svg ndIcon="search"></svg></div>
<h2 ndEmptyTitle>No results</h2>
<p ndEmptyDescription>
Nothing matches “{{ query() }}”. Check the spelling or try other
words.
</p>
</div>
<div ndEmptyContent>
<button ndButton variant="outline" (click)="query.set('')">
Clear search
</button>
</div>
</div>
`,
})
export class EmptySearchExample {
protected readonly query = signal('quarterly report 2019');
}
Empty State avatar
An empty state with an avatar as its media.
Lena is away
She'll reply to messages when she's back on Monday.
import { Component } from '@angular/core';
import { Avatar, AvatarFallback } from '@/ui/avatar/avatar';
import { Button } from '@/ui/button/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/ui/empty/empty';
/** An empty state with an avatar as its media, in the default style. */
@Component({
selector: 'example-empty-avatar',
imports: [
Avatar,
AvatarFallback,
Button,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
],
template: `
<div ndEmpty>
<div ndEmptyHeader>
<div ndEmptyMedia>
<span ndAvatar size="lg"><span ndAvatarFallback>LR</span></span>
</div>
<h2 ndEmptyTitle>Lena is away</h2>
<p ndEmptyDescription>
She'll reply to messages when she's back on Monday.
</p>
</div>
<div ndEmptyContent>
<button ndButton variant="outline" size="sm">Leave a message</button>
</div>
</div>
`,
})
export class EmptyAvatarExample {}
Accessibility
Empty State has no keyboard interaction or focus handling of its own; it keeps the semantics of the element it's on.
API reference
Empty
[ndEmpty]An empty state: shown where content would be, when there's none yet, with what to do about it.
| Empty binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty |
EmptyHeader
[ndEmptyHeader]An empty state's header: its media, title and description.
| EmptyHeader binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty-header |
EmptyMedia
[ndEmptyMedia]An image or icon above an empty state's title; variant="icon" puts an
icon on a muted tile.
| EmptyMedia binding | Type | Default |
|---|---|---|
[variant]Visual style. | 'default' | 'icon' | 'default' |
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty-icon |
data-variant | — |
EmptyTitle
[ndEmptyTitle]An empty state's title. Put it on the heading element that fits the page's outline.
| EmptyTitle binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty-title |
EmptyDescription
[ndEmptyDescription]What an empty state means, and what to do; links in it are underlined.
| EmptyDescription binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty-description |
EmptyContent
[ndEmptyContent]An empty state's actions, under its header.
| EmptyContent binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | empty-content |
Agent notes
Do
- Say what would be here, and how to get some: "No projects yet. Create one to get started."
- Give it one main action, such as a button that creates the first item.
- Tell search and filter results apart from a truly empty list, and offer to clear the filters.
Don't
- Don't leave a blank area or a table with only headers instead.
- Don't use it for errors; use an alert, which says what went wrong.