HextaUI

Empty

Um espaço reservado para telas que ainda não têm nada a mostrar, com ícone, mensagem e a próxima ação.

You’re all caught up
New mentions, reviews and replies land here. Nothing needs you right now.
import { IconInbox } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyDemo() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="stack">
          <IconInbox />
        </EmptyMedia>
        <EmptyTitle>You’re all caught up</EmptyTitle>
        <EmptyDescription>
          New mentions, reviews and replies land here. Nothing needs you right
          now.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Notification settings
        </Button>
      </EmptyContent>
    </Empty>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/empty.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="stack">
      <IconFolder />
    </EmptyMedia>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>Create a project to get started.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Create project</Button>
  </EmptyContent>
</Empty>
Empty
├── EmptyHeader
│   ├── EmptyMedia
│   ├── EmptyTitle
│   └── EmptyDescription
└── EmptyContent

Entrada

Quando um estado vazio substitui o conteúdo, suas partes surgem com fade uma após a outra: mídia, título, descrição e depois as ações. Dispense as notificações para ver. Com movimento reduzido, ele apenas aparece. Defina animated={false} para estados vazios que existem desde a primeira pintura.

"use client"

import * as React from "react"
import { IconBellOff, IconX } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

const initial = [
  "Jun approved your pull request",
  "Mira mentioned you in #design",
  "Deploy to production finished",
]

export function EmptyTransition() {
  const [items, setItems] = React.useState(initial)

  return (
    <div className="flex min-h-64 w-full max-w-sm flex-col">
      {items.length > 0 ? (
        <ul className="flex flex-col gap-1 rounded-xl border p-1.5 text-sm">
          {items.map((item) => (
            <li
              key={item}
              className="flex items-center justify-between gap-2 rounded-md py-1 ps-2.5"
            >
              <span className="min-w-0 truncate">{item}</span>
              <Button
                variant="ghost"
                size="icon-sm"
                aria-label={`Dismiss “${item}”`}
                onClick={() =>
                  setItems(items.filter((other) => other !== item))
                }
              >
                <IconX />
              </Button>
            </li>
          ))}
        </ul>
      ) : (
        <Empty variant="outline" size="sm">
          <EmptyHeader>
            <EmptyMedia variant="stack">
              <IconBellOff />
            </EmptyMedia>
            <EmptyTitle>No notifications</EmptyTitle>
            <EmptyDescription>
              Dismissed items are gone for good.
            </EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button
              variant="outline"
              size="sm"
              onClick={() => setItems(initial)}
            >
              Bring them back
            </Button>
          </EmptyContent>
        </Empty>
      )}
    </div>
  )
}

Outline

variant="outline" desenha uma borda fina tracejada, a forma habitual de marcar uma zona de soltar ou uma área à espera de conteúdo.

import { IconCloudUpload } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyOutline() {
  return (
    <Empty variant="outline">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconCloudUpload />
        </EmptyMedia>
        <EmptyTitle>Cloud storage is empty</EmptyTitle>
        <EmptyDescription>
          Upload files to keep them in sync across every device.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Upload files
        </Button>
      </EmptyContent>
    </Empty>
  )
}

Atenuado

variant="muted" fica sobre um preenchimento suave. O bloco do ícone passa para uma superfície branca elevada, para que continue se destacando.

import { IconBell } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyMuted() {
  return (
    <Empty variant="muted">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconBell />
        </EmptyMedia>
        <EmptyTitle>No notifications</EmptyTitle>
        <EmptyDescription>
          You’re all caught up. New notifications will appear here.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Notification settings
        </Button>
      </EmptyContent>
    </Empty>
  )
}

Pequeno

size="sm" reduz o padding, os espaços, o bloco do ícone e o título para sidebars, popovers e painéis estreitos.

import { IconInbox } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptySmall() {
  return (
    <div className="w-full max-w-xs">
      <Empty variant="outline" size="sm">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconInbox />
          </EmptyMedia>
          <EmptyTitle>No messages</EmptyTitle>
          <EmptyDescription>
            Messages from your team land here.
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <Button size="sm">New message</Button>
        </EmptyContent>
      </Empty>
    </div>
  )
}

Avatar

A variant padrão de EmptyMedia apenas centraliza seu conteúdo, então qualquer avatar, badge ou ilustração se encaixa.

import { Avatar, AvatarBadge, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyAvatar() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <Avatar size="lg">
            <AvatarFallback>LH</AvatarFallback>
            <AvatarBadge status="offline" />
          </Avatar>
        </EmptyMedia>
        <EmptyTitle>Lena is offline</EmptyTitle>
        <EmptyDescription>
          Leave a message and she’ll see it when she’s back.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">Leave a message</Button>
      </EmptyContent>
    </Empty>
  )
}

Grupo de avatares

Mostre quem poderia estar aqui, para convites e espaços compartilhados.

import { IconPlus } from "@tabler/icons-react"

import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyAvatarGroup() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <AvatarGroup>
            <Avatar>
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
            <Avatar>
              <AvatarFallback>GH</AvatarFallback>
            </Avatar>
            <Avatar>
              <AvatarFallback>AT</AvatarFallback>
            </Avatar>
          </AvatarGroup>
        </EmptyMedia>
        <EmptyTitle>No team members</EmptyTitle>
        <EmptyDescription>
          Invite your team to collaborate on this project.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">
          <IconPlus data-icon="inline-start" />
          Invite members
        </Button>
      </EmptyContent>
    </Empty>
  )
}

Imagem

Dê dimensões explícitas às imagens para que o layout não se desloque enquanto elas carregam.

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyImage() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <img
            src="/preview/landscape.svg"
            alt=""
            width={160}
            height={100}
            className="h-24 w-40 rounded-lg object-cover"
          />
        </EmptyMedia>
        <EmptyTitle>No photos in this album</EmptyTitle>
        <EmptyDescription>
          Drop photos here or add them from your library.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Add photos
        </Button>
      </EmptyContent>
    </Empty>
  )
}

Sem resultados de busca

Troque os resultados por um estado vazio quando os filtros não encontrarem nada, e ofereça uma saída. Um role="status" visualmente oculto anuncia a contagem, então usuários de leitor de tela ouvem a mudança sem que a lista inteira seja lida.

"use client"

import * as React from "react"
import { IconFilterOff } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

const tasks = [
  { title: "Write release notes", done: true },
  { title: "Review pull requests", done: true },
  { title: "Plan the next sprint", done: false },
]

export function EmptySearchResults() {
  const [onlyOpen, setOnlyOpen] = React.useState(true)
  const visible = tasks.filter((task) => !onlyOpen || !task.done)
  const [query, setQuery] = React.useState("sprint")
  const results = visible.filter((task) =>
    task.title.toLowerCase().includes(query.toLowerCase())
  )

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <Button
          variant={query ? "secondary" : "outline"}
          size="sm"
          onClick={() => setQuery(query ? "" : "sprint")}
        >
          {query ? `Search: ${query}` : "No search"}
        </Button>
        <Button
          variant={onlyOpen ? "secondary" : "outline"}
          size="sm"
          onClick={() => setOnlyOpen(!onlyOpen)}
        >
          {onlyOpen ? "Open tasks only" : "All tasks"}
        </Button>
        <Button variant="outline" size="sm" onClick={() => setQuery("design")}>
          Search “design”
        </Button>
      </div>
      <p role="status" className="sr-only">
        {results.length === 1 ? "1 task" : `${results.length} tasks`}
      </p>
      <div>
        {results.length > 0 ? (
          <ul className="flex flex-col gap-1 rounded-xl border p-2 text-sm">
            {results.map((task) => (
              <li key={task.title} className="rounded-md px-2 py-1.5">
                {task.title}
              </li>
            ))}
          </ul>
        ) : (
          <Empty variant="outline" size="sm">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <IconFilterOff />
              </EmptyMedia>
              <EmptyTitle>No matching tasks</EmptyTitle>
              <EmptyDescription>
                Nothing matches “{query}” with the current filters.
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button
                variant="outline"
                size="sm"
                onClick={() => {
                  setQuery("")
                  setOnlyOpen(false)
                }}
              >
                Clear filters
              </Button>
            </EmptyContent>
          </Empty>
        )}
      </div>
    </div>
  )
}

Em um card

Use size="sm" com a variant muted para preencher um card que ainda não tem dados.

import { IconChartBar } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyInCard() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Revenue</CardTitle>
        <CardDescription>Last 30 days</CardDescription>
      </CardHeader>
      <CardContent>
        <Empty variant="muted" size="sm">
          <EmptyHeader>
            <EmptyMedia variant="icon">
              <IconChartBar />
            </EmptyMedia>
            <EmptyTitle>No data yet</EmptyTitle>
            <EmptyDescription>
              Charts appear after your first sale.
            </EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button variant="outline" size="sm">
              Connect a store
            </Button>
          </EmptyContent>
        </Empty>
      </CardContent>
    </Card>
  )
}

Em uma tabela

Coloque-o em uma única célula que abrange todas as colunas com colSpan, para que o cabeçalho permaneça alinhado.

import { IconReceipt } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

export function EmptyInTable() {
  return (
    <div className="w-full">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Invoice</TableHead>
            <TableHead>Customer</TableHead>
            <TableHead>Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableRow>
            <TableCell colSpan={3}>
              <Empty size="sm">
                <EmptyHeader>
                  <EmptyMedia variant="icon">
                    <IconReceipt />
                  </EmptyMedia>
                  <EmptyTitle>No invoices</EmptyTitle>
                  <EmptyDescription>
                    Invoices you send will show up here.
                  </EmptyDescription>
                </EmptyHeader>
                <EmptyContent>
                  <Button size="sm">Create invoice</Button>
                </EmptyContent>
              </Empty>
            </TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </div>
  )
}

Em um popover

Um estado vazio compacto para menus e painéis de notificações.

import { IconBell, IconBellOff } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export function EmptyInPopover() {
  return (
    <Popover>
      <PopoverTrigger
        render={
          <Button variant="outline" size="icon" aria-label="Notifications" />
        }
      >
        <IconBell />
      </PopoverTrigger>
      <PopoverContent className="w-72">
        <Empty size="sm">
          <EmptyHeader>
            <EmptyMedia variant="icon">
              <IconBellOff />
            </EmptyMedia>
            <EmptyTitle>Nothing new</EmptyTitle>
            <EmptyDescription>
              We’ll let you know when something needs your attention.
            </EmptyDescription>
          </EmptyHeader>
        </Empty>
      </PopoverContent>
    </Popover>
  )
}

Em uma lista de comandos

<CommandEmpty /> passa a busca atual a uma função como filho, para que a mensagem possa citá-la.

"use client"

import * as React from "react"
import { IconSearch } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyInCommand() {
  const [search, setSearch] = React.useState("zzz")

  return (
    <Command className="w-full max-w-sm">
      <CommandInput
        placeholder="Search people…"
        value={search}
        onValueChange={setSearch}
      />
      <CommandList>
        <CommandEmpty>
          {(search) => (
            <Empty size="sm">
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <IconSearch />
                </EmptyMedia>
                <EmptyTitle>No results</EmptyTitle>
                <EmptyDescription>
                  Nothing matches “{search}”. Try another name.
                </EmptyDescription>
              </EmptyHeader>
            </Empty>
          )}
        </CommandEmpty>
        <CommandGroup heading="People">
          <CommandItem>Ada Lovelace</CommandItem>
          <CommandItem>Alan Turing</CommandItem>
          <CommandItem>Grace Hopper</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Nível do heading

O título é um <div> para nunca quebrar a estrutura da sua página. Renderize-o com o nível de heading adequado e rotule a região com ele.

import { IconFileText } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyHeading() {
  return (
    <Empty render={<section aria-labelledby="drafts-empty-title" />}>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconFileText />
        </EmptyMedia>
        <EmptyTitle id="drafts-empty-title" render={<h3 />}>
          No drafts
        </EmptyTitle>
        <EmptyDescription render={<p />}>
          Drafts are saved automatically while you write.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">Write a post</Button>
      </EmptyContent>
    </Empty>
  )
}

Conteúdo longo

Nomes sem quebra e endereços longos quebram dentro do contêiner, e rótulos longos de botão são truncados.

import { IconAlertTriangle } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyLongContent() {
  return (
    <div className="w-full max-w-72">
      <Empty variant="outline" size="sm">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconAlertTriangle />
          </EmptyMedia>
          <EmptyTitle>
            Nothing found in
            averyveryverylongunbrokenworkspacenamethatkeepsgoing
          </EmptyTitle>
          <EmptyDescription>
            <p>
              We searched every folder shared with
              [email protected] and found
              nothing.
            </p>
            <p>
              Check the <a href="#">sharing settings</a> or ask the owner for
              access.
            </p>
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <Button variant="outline" size="sm" className="max-w-full">
            <span className="truncate">
              Request access from the workspace owner
            </span>
          </Button>
        </EmptyContent>
      </Empty>
    </div>
  )
}

Da direita para a esquerda

Tudo é centralizado e usa propriedades lógicas, então é lido naturalmente em layouts da direita para a esquerda.

import { IconFolderPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyRtl() {
  return (
    <div dir="rtl" className="w-full">
      <Empty variant="outline">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconFolderPlus />
          </EmptyMedia>
          <EmptyTitle>لا توجد مشاريع بعد</EmptyTitle>
          <EmptyDescription>
            لم تنشئ أي مشروع حتى الآن. ابدأ مشروعًا جديدًا أو استورد مستودعًا
            موجودًا. <a href="#">اعرف المزيد</a>
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <div className="flex flex-wrap justify-center gap-2">
            <Button>إنشاء مشروع</Button>
            <Button variant="outline">استيراد</Button>
          </div>
        </EmptyContent>
      </Empty>
    </div>
  )
}
  • <EmptyTitle /> renderiza um <div>. Passe render={<h2 />} (ou o nível adequado) quando o estado vazio for uma seção da página.
  • <EmptyMedia variant="icon" /> fica oculto para tecnologias assistivas porque o título já diz o que ele significa. Imagens na variant padrão continuam expostas, então dê a elas um alt ou alt="".
  • Quando os resultados desaparecem após uma busca ou filtro, anuncie a mudança com uma mensagem curta de role="status" ao lado da lista. Não transforme o estado vazio inteiro em uma região live, pois seus botões e links seriam lidos a cada vez.

Cada parte renderiza um elemento simples, aceita seus atributos e suporta a prop render para trocá-lo.

PropTipoPadrão
variant
"default" | "outline" | "muted""default"
sizesm reduz o padding, os espaços, o bloco do ícone e o título.
"default" | "sm""default"
animatedFaz as partes surgirem com fade em sequência quando o estado vazio é montado. Ignorado com movimento reduzido.
booleantrue
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty"Selecione a raiz no CSS.
data-variantA variante atual.
data-sizeO tamanho atual.
data-animatedPresente quando a animação de entrada está ativada.
--empty-paddingPadding em todos os lados. Definido por size.
--empty-gapEspaço entre o header e o conteúdo. Definido por size.
PropTipoPadrão
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty-header"Selecione o cabeçalho no CSS.
PropTipoPadrão
varianticon coloca um ícone em um bloco atenuado. stack eleva o bloco sobre dois cards em leque que se abrem quando o mouse passa sobre o estado vazio. Ambos ocultam a mídia para tecnologias assistivas.
"default" | "icon" | "stack""default"
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty-icon"Seleciona a mídia no CSS. Compatível com o shadcn.
data-variantA variante atual.
PropTipoPadrão
renderRenderize um heading, como render={<h2 />}.
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty-title"Selecione o título no CSS.
PropTipoPadrão
renderLinks simples dentro dele são sublinhados. Componentes renderizados como links mantêm o próprio estilo.
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty-description"Selecione a descrição no CSS.
PropTipoPadrão
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="empty-content"Seleciona a área de ações no CSS.

Usado em blocos

Blocos que se baseiam em Empty.