Mục
Một hàng linh hoạt với các ô dành cho phương tiện, tiêu đề, mô tả và hành động, dùng cho các nguồn thông báo, danh sách cài đặt và các hàng tệp — theo phong cách neobrutalist.
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Item là một hàng flex với các khu vực được đặt tên — media, nội dung, hành động — dành cho kiểu “danh sách các thứ” mà ứng dụng nào rồi cũng cần đến. Bên dưới không có primitive headless nào, chỉ là những phần tử đã tạo kiểu (cộng thêm useRender của Base UI cho prop render), hoàn thiện theo công thức neobrutalist: viền 2px — nhìn thấy được ở biến thể outline — và vòng focus lệch cạnh, sắc nét.
Nên dùng khi:
- Dòng thông báo và hoạt động — ảnh đại diện, nội dung, dấu thời gian, kèm nút bỏ qua hoặc nút hành động ở mỗi hàng.
- Danh sách cài đặt và tích hợp — biểu tượng, tên, dòng trạng thái, cùng một switch hoặc nút ở cạnh cuối.
- Trình duyệt tệp và kết quả tìm kiếm — ảnh thu nhỏ hoặc biểu tượng kèm thông tin mô tả, cả hàng là một liên kết thông qua
render.
Component Item là một container flex đơn giản có thể chứa hầu như mọi loại nội dung. Dùng nó để hiển thị tiêu đề, mô tả và hành động. Nhóm nó với component ItemGroup để tạo một danh sách các mục.
Cài đặt#
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
Sao chép và dán đoạn mã sau vào dự án của bạn.
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,
}
Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.
Cách dùng#
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>Kết hợp#
Dùng cách kết hợp sau để xây dựng một Item:
ItemGroup
└── Item
├── ItemHeader
├── ItemMedia
├── ItemContent
│ ├── ItemTitle
│ └── ItemDescription
├── ItemActions
└── ItemFooterItem vs Field#
Dùng Field nếu bạn cần hiển thị một input biểu mẫu như checkbox, input, radio hoặc select.
Nếu bạn chỉ cần hiển thị nội dung như tiêu đề, mô tả và hành động, hãy dùng Item.
Variant#
Dùng prop variant để thay đổi kiểu hiển thị của mục.
import { InboxIcon } from "lucide-react"
import {Size#
Dùng prop size để thay đổi kích thước của mục. Các kích thước khả dụng là default, sm và xs.
import { InboxIcon } from "lucide-react"
import {Ví dụ#
Biểu tượng#
Dùng ItemMedia với variant="icon" để hiển thị một biểu tượng.
import { ShieldAlertIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Avatar#
Bạn có thể dùng ItemMedia với variant="avatar" để hiển thị một avatar.
import { Plus } from "lucide-react"
import {Hình ảnh#
Dùng ItemMedia với variant="image" để hiển thị một hình ảnh.
import Image from "next/image"
import {Nhóm#
Dùng ItemGroup để nhóm các mục liên quan với nhau.
import * as React from "react"
import { PlusIcon } from "lucide-react"
Phần đầu#
Dùng ItemHeader để thêm phần đầu phía trên nội dung của mục.
import Image from "next/image"
import {Liên kết#
Dùng prop render để render mục dưới dạng một liên kết. Các trạng thái hover và focus sẽ được áp dụng cho phần tử anchor.
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#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
"use client"
import * as React from "react"Khả năng truy cập#
Item chỉ mang tính trình bày — là những phần tử đã tạo kiểu, không tự kết nối ARIA nào, dù ItemGroup có render role="list" (thêm role="listitem" cho từng Item nếu bạn muốn cấu trúc danh sách được đọc lên). Với các hàng có thể tương tác, đừng gắn trình xử lý click lên thẻ div — hãy render một <a> hoặc <button> thật thông qua prop render để nó có focus và hành vi bàn phím native; vòng focus hiển thị thì đã được tạo kiểu sẵn. Hãy đánh dấu media dạng biểu tượng mang tính trang trí bằng aria-hidden, và cho media dạng ảnh một dòng alt có ý nghĩa.
Tham chiếu API#
Item#
Component chính để hiển thị nội dung với media, tiêu đề, mô tả và hành động.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "outline" | "muted" | "default" |
size | "default" | "sm" | "xs" | "default" |
render | React.ReactElement |
ItemGroup#
Một container nhóm các mục liên quan với nhau bằng kiểu dáng nhất quán.
<ItemGroup>
<Item />
<Item />
</ItemGroup>ItemSeparator#
Một dấu phân cách giữa các mục trong một nhóm.
<ItemGroup>
<Item />
<ItemSeparator />
<Item />
</ItemGroup>ItemMedia#
Dùng ItemMedia để hiển thị nội dung media như biểu tượng, hình ảnh hoặc avatar.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "icon" | "image" | "default" |
<ItemMedia variant="icon">
<Icon />
</ItemMedia><ItemMedia variant="image">
<img src="..." alt="..." />
</ItemMedia>ItemContent#
Bao bọc tiêu đề và mô tả của mục.
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>ItemTitle#
Hiển thị tiêu đề của mục.
<ItemTitle>Item Title</ItemTitle>ItemDescription#
Hiển thị mô tả của mục.
<ItemDescription>Item description</ItemDescription>ItemActions#
Container cho các nút hành động hoặc các phần tử tương tác khác.
<ItemActions>
<Button>Action</Button>
</ItemActions>ItemHeader#
Hiển thị phần đầu phía trên nội dung của mục.
<Item>
<ItemHeader>Header</ItemHeader>
<ItemContent>...</ItemContent>
</Item>ItemFooter#
Hiển thị phần chân phía dưới nội dung của mục.
<Item>
<ItemContent>...</ItemContent>
<ItemFooter>Footer</ItemFooter>
</Item>