HextaUI

Empty

Un espace réservé pour les écrans sans contenu à afficher, avec une icône, un message et l’action suivante.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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

Entrée

Lorsqu’un état vide remplace du contenu, ses parties apparaissent en fondu l’une après l’autre : média, titre, description, puis actions. Fermez les notifications pour le voir. Avec la réduction des animations, il apparaît simplement. Définissez animated={false} pour les états vides présents dès le premier rendu.

"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" dessine une bordure fine en pointillés, la manière habituelle de marquer une zone de dépôt ou une zone en attente de contenu.

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>
  )
}

Atténué

variant="muted" repose sur un fond doux. La tuile d’icône passe à une surface blanche surélevée pour rester visible.

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>
  )
}

Petit

size="sm" resserre le remplissage, les écarts, la tuile d’icône et le titre pour les barres latérales, les popovers et les panneaux étroits.

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

La variante EmptyMedia par défaut ne fait que centrer son contenu : n’importe quel avatar, badge ou illustration convient.

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>
  )
}

Groupe d’avatars

Montrez qui pourrait être là, pour les invitations et les espaces partagés.

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>
  )
}

Image

Donnez aux images des dimensions explicites pour que la mise en page ne bouge pas pendant leur chargement.

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>
  )
}

Aucun résultat de recherche

Remplacez les résultats par un état vide lorsque les filtres ne correspondent à rien, et proposez une issue. Un role="status" masqué visuellement annonce le nombre : les utilisateurs de lecteurs d’écran entendent le changement sans que toute la liste soit lue.

"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>
  )
}

Dans une carte

Utilisez size="sm" avec la variante muted pour remplir une carte qui n’a pas encore de données.

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>
  )
}

Dans un tableau

Placez-le dans une seule cellule qui couvre toutes les colonnes avec colSpan, pour que l’en-tête reste aligné.

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>
  )
}

Dans un popover

Un état vide compact pour les menus et les panneaux de notifications.

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>
  )
}

Dans une liste de commandes

<CommandEmpty /> transmet la recherche actuelle à un enfant fonction, pour que le message puisse la citer.

"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>
  )
}

Niveau de titre

Le titre est un <div> pour ne jamais casser le plan de votre page. Rendez-le avec le niveau de titre adapté, et étiquetez la région avec lui.

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>
  )
}

Contenu long

Les noms sans coupure et les longues adresses passent à la ligne dans le conteneur, et les libellés de bouton longs sont tronqués.

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>
  )
}

De droite à gauche

Tout est centré et utilise des propriétés logiques : il se lit donc naturellement dans les mises en page de droite à gauche.

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 /> rend un <div>. Passez render={<h2 />} (ou le niveau adapté) lorsque l’état vide est une section de la page.
  • <EmptyMedia variant="icon" /> est masqué aux technologies d’assistance, car le titre dit déjà ce qu’il signifie. Les images de la variante par défaut restent exposées : donnez-leur un alt ou alt="".
  • Lorsque des résultats disparaissent après une recherche ou un filtre, annoncez le changement par un court message role="status" à côté de la liste. Ne faites pas de tout l’état vide une région live, car ses boutons et ses liens seraient lus à chaque fois.

Chaque partie rend un élément simple, accepte ses attributs et prend en charge la prop render pour le remplacer.

PropTypePar défaut
variant
"default" | "outline" | "muted""default"
sizesm resserre le remplissage, les écarts, la tuile d’icône et le titre.
"default" | "sm""default"
animatedFait apparaître les parties en fondu l’une après l’autre au montage de l’état vide. Ignoré avec la réduction des animations.
booleantrue
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty"Ciblez la racine en CSS.
data-variantLa variante actuelle.
data-sizeLa taille actuelle.
data-animatedPrésent lorsque l’animation d’entrée est activée.
--empty-paddingRemplissage de tous les côtés. Défini par size.
--empty-gapEspace entre l’en-tête et le contenu. Défini par size.
PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty-header"Ciblez l’en-tête en CSS.
PropTypePar défaut
varianticon place une icône dans une tuile atténuée. stack élève la tuile au-dessus de deux cartes en éventail qui s’écartent au survol de l’état vide. Les deux masquent le média aux technologies d’assistance.
"default" | "icon" | "stack""default"
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty-icon"Ciblez le média en CSS. Compatible avec shadcn.
data-variantLa variante actuelle.
PropTypePar défaut
renderRendez un titre, par exemple render={<h2 />}.
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty-title"Ciblez le titre en CSS.
PropTypePar défaut
renderLes liens simples à l’intérieur sont soulignés. Les composants rendus comme liens gardent leur propre style.
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty-description"Ciblez la description en CSS.
PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="empty-content"Ciblez la zone d’actions en CSS.

Utilisé dans les blocks

Des blocks qui s’appuient sur Empty.