Bỏ qua đến nội dung

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.json
npx shadcn@latest add https://neobrutalism.com/r/base/item.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/item.json
bunx --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.

components/ui/item.tsx
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
    └── ItemFooter

Item 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, smxs.

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>
"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.

PropTypeDefault
variant"default" | "outline" | "muted""default"
size"default" | "sm" | "xs""default"
renderReact.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.

PropTypeDefault
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>