Bỏ qua đến nội dung

Thẻ

Một khung có viền dành cho bảng điều khiển, các gói giá và bảng danh sách sản phẩm — bao gồm phần đầu trang, nội dung và phần chân trang, được áp dụng hiệu ứng bóng cứng theo phong cách tân thô mộc.

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

Card nhóm các nội dung liên quan vào một khối độc lập, với các slot header, content và footer tùy chọn. Không có primitive nào bên dưới — đây chỉ là markup thuần được tạo kiểu theo công thức neobrutalist: một đường viền dày, một bóng đổ lệch đậm và tiêu đề dùng phông tiêu đề.

Nên dùng khi:

  • Panel dashboard — ô số liệu, biểu đồ và dòng hoạt động xếp thành lưới.
  • Các hạng giá — tên gói và giá ở header, tính năng ở phần thân, nút mua ở footer.
  • Form độc lập — các khối đăng nhập, đăng ký và cài đặt cần một đường bao thị giác.

Cài đặt

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

Sao chép và dán đoạn mã sau vào dự án của bạn.

components/ui/card.tsx
import * as React from "react"

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

function Card({
  className,
  size = "default",
  ...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
  return (
    <div
      data-slot="card"
      data-size={size}
      className={cn(
        "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded border-2 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-md [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
        className
      )}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
        className
      )}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-title"
      className={cn(
        "cn-font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
        className
      )}
      {...props}
    />
  )
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-action"
      className={cn(
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
        className
      )}
      {...props}
    />
  )
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn("px-(--card-spacing)", className)}
      {...props}
    />
  )
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn(
        "flex items-center rounded-b-xl border-t-2 bg-muted/50 p-(--card-spacing)",
        className
      )}
      {...props}
    />
  )
}

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardContent,
}

Cập nhật các đường dẫn import cho khớp với cấu trúc dự án của bạn.

Cách dùng

import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card Description</CardDescription>
    <CardAction>Card Action</CardAction>
  </CardHeader>
  <CardContent>
    <p>Card Content</p>
  </CardContent>
  <CardFooter>
    <p>Card Footer</p>
  </CardFooter>
</Card>

Kết hợp

Dùng cách kết hợp sau để dựng một Card:

Card
├── CardHeader
│   ├── CardTitle
│   ├── CardDescription
│   └── CardAction
├── CardContent
└── CardFooter

Ví dụ

Kích thước

Dùng prop size="sm" để đặt kích thước card thành nhỏ. Biến thể kích thước nhỏ dùng khoảng cách hẹp hơn.

import { ChevronRightIcon } from "lucide-react"

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

Khoảng cách

Ngoài prop size, bạn có thể dùng biến CSS --card-spacing để kiểm soát khoảng cách giữa các phần và phần đệm trong của các bộ phận card.

"use client"

import * as React from "react"

Dùng lề âm với -mx-(--card-spacing) để nội dung trải từ cạnh này sang cạnh kia mà vẫn thẳng hàng với phần đệm trong của card. Khi nội dung trải cạnh-đến-cạnh nằm phía trên một phần chân, hãy dùng -mb-(--card-spacing) trên CardContent để bỏ khoảng trống giữa các phần.

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

Hình ảnh

Thêm một hình ảnh trước phần đầu card để tạo một card có hình ảnh.

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

RTL

Để bật hỗ trợ RTL trong Neobrutalism, xem hướng dẫn cấu hình RTL.

"use client"

import * as React from "react"

Khả năng truy cập

Card hoàn toàn mang tính trình bày — mọi phần đều render một <div> thuần không có role ngầm định, nên nó không thêm gì vào cây khả năng truy cập. CardTitle không phải một phần tử tiêu đề: hãy lồng một <h2><h4> thật vào trong nó khi tiêu đề cần xuất hiện trong cấu trúc phân cấp của tài liệu. Với card có thể nhấp, hãy đặt liên kết hoặc nút vào bên trong card rồi trải vùng bấm của nó ra khắp bề mặt — đừng bao giờ gắn onClick lên chính <div> của card.

Tham chiếu API

Card

Thành phần Card là khối chứa gốc cho nội dung card.

PropTypeDefault
size"default" | "sm""default"
classNamestring-

CardHeader

Thành phần CardHeader được dùng cho tiêu đề, mô tả và một hành động tùy chọn.

PropTypeDefault
classNamestring-

CardTitle

Thành phần CardTitle được dùng cho tiêu đề của card.

PropTypeDefault
classNamestring-

CardDescription

Thành phần CardDescription được dùng cho văn bản trợ giúp bên dưới tiêu đề.

PropTypeDefault
classNamestring-

CardAction

Thành phần CardAction đặt nội dung ở góc trên bên phải của phần đầu (ví dụ, một nút hoặc một badge).

PropTypeDefault
classNamestring-

CardContent

Thành phần CardContent được dùng cho phần thân chính của card.

PropTypeDefault
classNamestring-

CardFooter

Thành phần CardFooter được dùng cho các hành động và nội dung phụ ở phía dưới card.

PropTypeDefault
classNamestring-

Nhật ký thay đổi

Biến khoảng cách

Nếu bạn đang nâng cấp từ một phiên bản trước của thành phần Card, bạn sẽ cần áp dụng các cập nhật sau để dùng biến --card-spacing:

Cập nhật các lớp khoảng cách của gốc Card.

Thay gap và padding dọc được viết cứng bằng --card-spacing, và đặt các giá trị kích thước mặc định và nhỏ ở gốc:

  className={cn(
-   "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+   "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
    className
  )}

Cập nhật các lớp khoảng cách của CardHeader.

Thay padding ngang và khoảng cách viền bằng biến dùng chung:

  className={cn(
-   "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
+   "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
    className
  )}

Cập nhật các lớp khoảng cách của CardContent và CardFooter.

Dùng --card-spacing cho phần đệm trong của nội dung và padding của phần chân:

  function CardContent({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-content"
-       className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
+       className={cn("px-(--card-spacing)", className)}
        {...props}
      />
    )
  }
  className={cn(
-   "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
+   "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
    className
  )}

Sau khi áp dụng các thay đổi này, bạn có thể tùy chỉnh khoảng cách của card bằng cách đặt --card-spacing trên Card với một lớp thuộc tính tùy ý:

function Example() {
  return <Card className="[--card-spacing:--spacing(6)]">...</Card>
}