import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Item은 media, content, actions 같은 이름 있는 슬롯을 가진 flex 행으로, 앱마다 결국 마주치는 "목록 항목" 패턴을 담당합니다. 헤드리스 프리미티브는 없고 스타일된 요소만 있으며(Base UI의 useRender로 render prop을 지원하는 형태), 네오브루탈리즘 공식에 맞춰 마무리했습니다. outline 변형에서 보이는 2px 테두리와 선명하게 오프셋된 포커스 링이 있습니다.
다음과 같은 경우에 사용합니다.
- 알림·활동 피드 — 행마다 아바타, 메시지, 타임스탬프, 닫기나 동작 버튼.
- 설정·연동 목록 — 아이콘, 이름, 상태 줄, 끝쪽에 스위치나 버튼.
- 파일 브라우저·검색 결과 — 썸네일이나 아이콘과 메타데이터, 행 전체를
render로 링크로 만듭니다.
Item 컴포넌트는 거의 모든 종류의 콘텐츠를 담을 수 있는 간단한 flex 컨테이너입니다. 제목, 설명, 액션을 표시하는 데 사용합니다. ItemGroup 컴포넌트로 묶으면 항목 목록을 만들 수 있습니다.
설치#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/item.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/item.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/item.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/item.json
다음 코드를 복사해서 프로젝트에 붙여넣습니다.
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn(
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
className
)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-2", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex w-full flex-wrap items-center rounded border-2 text-sm transition-colors duration-100 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary [a]:transition-colors [a]:hover:bg-accent",
{
variants: {
variant: {
default: "border-transparent",
outline: "border-border",
muted: "border-transparent bg-muted/50",
},
size: {
default: "gap-2.5 px-3 py-2.5",
sm: "gap-2.5 px-3 py-2.5",
xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
{
variants: {
variant: {
default: "bg-transparent",
icon: "[&_svg:not([class*='size-'])]:size-4",
image:
"size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}
import 경로를 프로젝트 구성에 맞게 수정합니다.
사용법#
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"<Item>
<ItemMedia variant="icon">
<Icon />
</ItemMedia>
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button>Action</Button>
</ItemActions>
</Item>구성#
Item을 만들려면 다음 구성을 사용합니다.
ItemGroup
└── Item
├── ItemHeader
├── ItemMedia
├── ItemContent
│ ├── ItemTitle
│ └── ItemDescription
├── ItemActions
└── ItemFooterItem vs Field#
체크박스, 입력, 라디오, 셀렉트 같은 폼 입력을 표시해야 한다면 Field를 사용합니다.
제목, 설명, 액션 같은 콘텐츠만 표시하면 된다면 Item을 사용합니다.
변형#
variant prop을 사용해서 항목의 시각적 스타일을 변경합니다.
import { InboxIcon } from "lucide-react"
import {크기#
size prop을 사용해서 항목의 크기를 변경합니다. 사용할 수 있는 크기는 default, sm, xs입니다.
import { InboxIcon } from "lucide-react"
import {예제#
아이콘#
아이콘을 표시하려면 ItemMedia에 variant="icon"을 지정합니다.
import { ShieldAlertIcon } from "lucide-react"
import { Button } from "@/components/ui/button"아바타#
아바타를 표시하려면 ItemMedia에 variant="avatar"를 지정합니다.
import { Plus } from "lucide-react"
import {이미지#
이미지를 표시하려면 ItemMedia에 variant="image"를 지정합니다.
import Image from "next/image"
import {그룹#
관련된 항목을 함께 묶으려면 ItemGroup을 사용합니다.
import * as React from "react"
import { PlusIcon } from "lucide-react"
헤더#
항목 콘텐츠 위에 헤더를 추가하려면 ItemHeader를 사용합니다.
import Image from "next/image"
import {링크#
render prop을 사용해서 항목을 링크로 렌더링합니다. 호버와 포커스 상태는 앵커 요소에 적용됩니다.
import { ChevronRightIcon, ExternalLinkIcon } from "lucide-react"
import {<Item render={<a href="/dashboard" />}>
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</Item>Dropdown#
"use client"
import { ChevronDownIcon } from "lucide-react"RTL#
Neobrutalism에서 RTL 지원을 활성화하는 방법은 RTL 설정 가이드를 참고하세요.
"use client"
import * as React from "react"접근성#
Item은 표현용입니다. 자체 ARIA 배선이 없는 스타일된 요소이며, ItemGroup은 role="list"를 렌더링합니다(목록 구조를 읽혀야 하면 각 Item에 role="listitem"을 추가). 상호작용 행에는 div에 클릭 핸들러를 두지 마세요. render prop로 실제 <a>나 <button>을 렌더링해 네이티브 포커스와 키보드 동작을 받게 하세요. 보이는 포커스 링은 이미 스타일되어 있습니다. 장식용 아이콘 미디어에는 aria-hidden, 이미지 미디어에는 의미 있는 alt 텍스트를 두세요.
API 참조#
Item#
미디어, 제목, 설명, 액션을 갖춘 콘텐츠를 표시하는 기본 컴포넌트입니다.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "outline" | "muted" | "default" |
size | "default" | "sm" | "xs" | "default" |
render | React.ReactElement |
ItemGroup#
관련된 항목을 일관된 스타일로 묶는 컨테이너입니다.
<ItemGroup>
<Item />
<Item />
</ItemGroup>ItemSeparator#
그룹 내 항목 사이에 놓이는 구분선입니다.
<ItemGroup>
<Item />
<ItemSeparator />
<Item />
</ItemGroup>ItemMedia#
아이콘, 이미지, 아바타 같은 미디어 콘텐츠를 표시하려면 ItemMedia를 사용합니다.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "icon" | "image" | "default" |
<ItemMedia variant="icon">
<Icon />
</ItemMedia><ItemMedia variant="image">
<img src="..." alt="..." />
</ItemMedia>ItemContent#
항목의 제목과 설명을 감쌉니다.
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>ItemTitle#
항목의 제목을 표시합니다.
<ItemTitle>Item Title</ItemTitle>ItemDescription#
항목의 설명을 표시합니다.
<ItemDescription>Item description</ItemDescription>ItemActions#
액션 버튼이나 그 밖의 상호작용 요소를 위한 컨테이너입니다.
<ItemActions>
<Button>Action</Button>
</ItemActions>ItemHeader#
항목 콘텐츠 위에 헤더를 표시합니다.
<Item>
<ItemHeader>Header</ItemHeader>
<ItemContent>...</ItemContent>
</Item>ItemFooter#
항목 콘텐츠 아래에 푸터를 표시합니다.
<Item>
<ItemContent>...</ItemContent>
<ItemFooter>Footer</ItemFooter>
</Item>