콘텐츠로 건너뛰기

빈 상태

검색 결과 없음, 빈 대시보드, 첫 실행 화면 등을 위한 자리 표시자 — 네오브루탈리즘 스타일의 테두리로 둘러싸인 아이콘, 제목 및 동작.

import { IconFolderCode } from "@tabler/icons-react"
import { ArrowUpRightIcon } from "lucide-react"

빈 상태는 렌더링할 내용이 없을 때 그 자리를 채웁니다. 아이콘이나 아바타, 제목, 설명, 행동 유도 버튼을 위한 슬롯이 있습니다. 하위 프리미티브 없이 조합 가능한 div 집합이며, 두꺼운 테두리와 굵은 제목 서체로 네오브루탈리즘 공식을 따릅니다.

다음과 같은 경우에 사용합니다.

  • 검색·필터 결과 없음 — 목록이 비어 있는 이유를 설명하고 초기화하거나 조건을 넓힐 방법을 안내합니다.
  • 첫 실행 화면 — "아직 프로젝트가 없습니다"와 바로 다음 단계로 이어지는 만들기 버튼.
  • 비워진 받은편지함과 큐 — 빈 목록이 오류가 아니라 성공 상태임을 확인시켜 줍니다.

설치

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/empty.json
npx shadcn@latest add https://neobrutalism.com/r/base/empty.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/empty.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/empty.json

다음 코드를 복사해 프로젝트에 붙여넣습니다.

components/ui/empty.tsx
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

function Empty({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty"
      className={cn(
        "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded border-2 bg-card p-6 text-center text-balance",
        className
      )}
      {...props}
    />
  )
}

function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-header"
      className={cn("flex max-w-sm flex-col items-center gap-2", className)}
      {...props}
    />
  )
}

const emptyMediaVariants = cva(
  "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function EmptyMedia({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
  return (
    <div
      data-slot="empty-icon"
      data-variant={variant}
      className={cn(emptyMediaVariants({ variant, className }))}
      {...props}
    />
  )
}

function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-title"
      className={cn(
        "cn-font-heading text-sm font-medium tracking-tight",
        className
      )}
      {...props}
    />
  )
}

function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <div
      data-slot="empty-description"
      className={cn(
        "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
        className
      )}
      {...props}
    />
  )
}

function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-content"
      className={cn(
        "flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
        className
      )}
      {...props}
    />
  )
}

export {
  Empty,
  EmptyHeader,
  EmptyTitle,
  EmptyDescription,
  EmptyContent,
  EmptyMedia,
}

임포트 경로를 프로젝트 구성에 맞게 수정합니다.

사용법

import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <Icon />
    </EmptyMedia>
    <EmptyTitle>No data</EmptyTitle>
    <EmptyDescription>No data found</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Add data</Button>
  </EmptyContent>
</Empty>

구성

Empty 상태를 구축하려면 다음 구성을 사용합니다.

Empty
├── EmptyHeader
│   ├── EmptyMedia
│   ├── EmptyTitle
│   └── EmptyDescription
└── EmptyContent

예제

아웃라인

border 유틸리티 클래스를 사용해 테두리가 있는 빈 상태를 만듭니다.

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

import { Button } from "@/components/ui/button"

배경

bg-*bg-gradient-* 유틸리티를 사용해 빈 상태에 배경을 추가합니다.

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

아바타

EmptyMedia 컴포넌트를 사용해 빈 상태에 아바타를 표시합니다.

import {
  Avatar,
  AvatarFallback,

아바타 그룹

EmptyMedia 컴포넌트를 사용해 빈 상태에 아바타 그룹을 표시합니다.

import { PlusIcon } from "lucide-react"

import {

InputGroup

EmptyContent 컴포넌트에 InputGroup 컴포넌트를 추가할 수 있습니다.

import { SearchIcon } from "lucide-react"

import {

RTL

Neobrutalism에서 RTL 지원을 활성화하는 방법은 RTL 설정 가이드를 참고하세요.

"use client"

import * as React from "react"

접근성

빈 상태는 표현용입니다. 역할이나 키보드 동작이 없는 일반 div라서 별도 배선은 없습니다. 장식용 EmptyMedia 아이콘에는 aria-hidden="true"를 두고, EmptyContent의 동작은 포커스 가능한 실제 Button이나 링크로 두세요. 비동기 작업 뒤(검색 결과 없음 등) 빈 상태가 콘텐츠를 대체할 때는 live region으로 알리세요. DOM 교체 자체는 스크린 리더에 들리지 않습니다.

API 참조

Empty

빈 상태의 메인 컴포넌트입니다. EmptyHeaderEmptyContent 컴포넌트를 감쌉니다.

PropTypeDefault
classNamestring
<Empty>
  <EmptyHeader />
  <EmptyContent />
</Empty>

EmptyHeader

EmptyHeader 컴포넌트는 빈 상태의 미디어, 제목, 설명을 감쌉니다.

PropTypeDefault
classNamestring
<EmptyHeader>
  <EmptyMedia />
  <EmptyTitle />
  <EmptyDescription />
</EmptyHeader>

EmptyMedia

EmptyMedia 컴포넌트를 사용해 아이콘이나 이미지 같은 빈 상태의 미디어를 표시합니다. 아바타 같은 다른 컴포넌트를 표시하는 데에도 사용할 수 있습니다.

PropTypeDefault
variant"default" | "icon"default
classNamestring
<EmptyMedia variant="icon">
  <Icon />
</EmptyMedia>
<EmptyMedia>
  <Avatar>
    <AvatarImage src="..." />
    <AvatarFallback>CN</AvatarFallback>
  </Avatar>
</EmptyMedia>

EmptyTitle

EmptyTitle 컴포넌트를 사용해 빈 상태의 제목을 표시합니다.

PropTypeDefault
classNamestring
<EmptyTitle>No data</EmptyTitle>

EmptyDescription

EmptyDescription 컴포넌트를 사용해 빈 상태의 설명을 표시합니다.

PropTypeDefault
classNamestring
<EmptyDescription>You do not have any notifications.</EmptyDescription>

EmptyContent

EmptyContent 컴포넌트를 사용해 버튼, 입력, 링크 같은 빈 상태의 콘텐츠를 표시합니다.

PropTypeDefault
classNamestring
<EmptyContent>
  <Button>Add Project</Button>
</EmptyContent>