Skip to main content

Empty

Present an empty or unavailable application state with optional media and actions.

Empty is an application-level presentation primitive. It provides a consistent layout for an empty-state message, optional media, and actions. Your application decides when to show it; Empty does not inspect collections or search results.

Basic usage#

import { Empty } from 'moraine'

export function Example() {
  return (
    <Empty>
      <Empty.Title>No projects yet</Empty.Title>
      <Empty.Description>Create a project to organize your work.</Empty.Description>
    </Empty>
  )
}

Playground#

No projects yet

Create a project to organize your work.

Learn more
Props
Slots

Anatomy#

Empty [component; slot=root]
├── Empty.Media [part; slot=media]
├── Empty.Title [part; slot=title]
├── Empty.Description [part; slot=description]
└── Empty.Actions [part; slot=actions]

Usage#

Compose only the parts you need. All parts belong inside Empty. Media accepts arbitrary JSX, including Icon, Avatar, AvatarGroup, and images; it does not impose a fixed media size or background.

Empty.Title renders a div by default. Choose an explicit heading such as as="h2" when the surrounding page needs one. Empty adds no role or ARIA relationships automatically.

The sm, md, and lg sizes change padding, spacing, typography, and action gaps together. Use sm within a Card or table, md for ordinary content, and lg for a page-level empty state. Vertical padding is more generous than horizontal insets. Title and description stay grouped, with more space around media and actions. Size does not resize the content placed inside Media.

Examples#

Basic empty state#

import { Empty, Icon } from 'moraine'

export function Basic() {
  return (
    <Empty>
      <Empty.Media>
        <Icon name="i-lucide-folder-open" class="text-muted-foreground size-8" />
      </Empty.Media>
      <Empty.Title>No projects yet</Empty.Title>
      <Empty.Description>Create a project to organize your work.</Empty.Description>
    </Empty>
  )
}

Actions#

Use actions to offer the next useful step. Wire them to your application’s handlers.

import { Button, Empty, Icon } from 'moraine'

export function Actions() {
  return (
    <Empty>
      <Empty.Media>
        <Icon name="i-lucide-inbox" class="text-muted-foreground size-8" />
      </Empty.Media>
      <Empty.Title>Your inbox is empty</Empty.Title>
      <Empty.Description>Start a conversation with your team.</Empty.Description>
      <Empty.Actions>
        <Button>New message</Button>
        <Button as="a" href="#basic-usage" variant="outline">
          Learn more
        </Button>
      </Empty.Actions>
    </Empty>
  )
}

No search results#

Your search logic decides when to render this message.

import { Button, Empty, Icon, Input } from 'moraine'
import { createSignal } from 'solid-js'

export function NoResults() {
  const [search, setSearch] = createSignal('design systems')
  return (
    <div class="max-w-md w-full">
      <Input
        value={search()}
        onInput={(event) => setSearch(event.currentTarget.value)}
        aria-label="Search projects"
        placeholder="Search projects"
      />
      <Empty size="sm">
        <Empty.Media>
          <Icon name="i-lucide-search" class="text-muted-foreground size-6" />
        </Empty.Media>
        <Empty.Title>No matching projects</Empty.Title>
        <Empty.Description>Try a different keyword or clear your search.</Empty.Description>
        <Empty.Actions>
          <Button variant="outline" size="sm" disabled={!search()} onClick={() => setSearch('')}>
            Clear search
          </Button>
        </Empty.Actions>
      </Empty>
    </div>
  )
}

Rich media#

Compose avatars or your own illustration without changing Empty’s layout API.

import { AvatarGroup, Button, Empty } from 'moraine'

export function RichMedia() {
  return (
    <Empty>
      <Empty.Media>
        <AvatarGroup
          items={[
            { text: 'Alex', alt: 'Alex' },
            { text: 'Sam', alt: 'Sam' },
            { text: 'Jo', alt: 'Jo' },
          ]}
          size="lg"
        />
      </Empty.Media>
      <Empty.Title>No team members yet</Empty.Title>
      <Empty.Description>Invite your collaborators to work together.</Empty.Description>
      <Empty.Actions>
        <Button>Invite members</Button>
      </Empty.Actions>
    </Empty>
  )
}

Sizes#

import { Empty, Icon } from 'moraine'
import { For } from 'solid-js'

export function Sizes() {
  return (
    <div class="w-full divide-border divide-y">
      <For each={['sm', 'md', 'lg'] as const}>
        {(size) => (
          <Empty size={size}>
            <Empty.Media>
              <Icon name="i-lucide-files" class="text-muted-foreground size-6" />
            </Empty.Media>
            <Empty.Title>No files ({size})</Empty.Title>
            <Empty.Description>Uploaded files will appear here.</Empty.Description>
          </Empty>
        )}
      </For>
    </div>
  )
}

Custom container styling#

Empty has no default border, background, shadow, or fixed height. Apply container styling through class and style; use root classes and styles to customize individual parts throughout the family. A part’s own class and style take precedence over those family overrides.

import { Button, Empty, Icon } from 'moraine'

export function CustomContainer() {
  return (
    <Empty
      as="section"
      aria-label="Saved reports"
      class="border border-border rounded-xl border-dashed bg-muted/30"
      classes={{ description: 'max-w-xs' }}
    >
      <Empty.Media>
        <Icon name="i-lucide-chart-no-axes-combined" class="text-muted-foreground size-8" />
      </Empty.Media>
      <Empty.Title as="h2">No saved reports</Empty.Title>
      <Empty.Description>
        Save a report to revisit the metrics that matter to you.
      </Empty.Description>
      <Empty.Actions>
        <Button variant="outline">Create report</Button>
      </Empty.Actions>
    </Empty>
  )
}

Props#

Empty#

Props for Empty. Renders a <div> element by default.

Prop

Media#

Renders a <div> element by default.

Prop

Title#

Renders a <div> element by default.

Prop

Description#

Renders a <p> element by default.

Prop

Actions#

Renders a <div> element by default.

Prop