Card
A flat tile separated by tone, not borders, with the system's title, meta and body roles.
Live
Online store
Q 48.2k
Installation
$ pnpm dlx shadcn@latest add @ssntl/cardUsage
import { Card, CardBody, CardSpacer, CardTitle, CardWell } from "@/components/ui/card"
<Card tone="ground">
<CardWell><Store /></CardWell>
<CardSpacer />
<CardTitle>Online store</CardTitle>
<CardBody>Orders, payments and inventory in one place.</CardBody>
</Card>- Tone separates, never borders: muted inside a white surface, ground on the page, one widget tile per grid.
- The system's type roles: title text-heading, meta text-label, body text-body.
- Content sets the height; tiles in a row stretch to the tallest so their bottoms line up.
Code
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// Essential Design `es-card`: a flat tile separated by tone, never by borders.
// `radius-lg`, tight padding so the content fills it, content sets the height.
const cardVariants = cva(
"relative flex min-w-0 flex-col gap-2 rounded-lg text-foreground",
{
variants: {
tone: {
// Inside a white surface.
muted: "bg-muted",
// Directly on the page background.
ground: "bg-card",
// The one dark tile of a bento grid, in both themes.
widget: "bg-black text-neutral-50 shadow-edge",
},
size: {
sm: "rounded-md p-5",
md: "p-6",
lg: "gap-3 p-8",
},
},
defaultVariants: { tone: "muted", size: "md" },
}
)
function Card({
tone,
size,
className,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof cardVariants>) {
return (
<div
data-slot="card"
data-tone={tone ?? "muted"}
className={cn(cardVariants({ tone, size, className }))}
{...props}
/>
)
}
/** A 44px round icon well at the top of the card. */
function CardWell({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-well"
className={cn(
"grid size-11 shrink-0 place-items-center rounded-full bg-secondary text-foreground [&_svg]:size-5",
"in-data-[tone=widget]:bg-card in-data-[tone=widget]:text-foreground",
className
)}
{...props}
/>
)
}
/** Pushes the content to the bottom of the card (at least spacing-4). */
function CardSpacer({ className, ...props }: React.ComponentProps<"div">) {
return (
<div aria-hidden className={cn("min-h-4 flex-1", className)} {...props} />
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
return (
<h3
data-slot="card-title"
className={cn("text-heading", className)}
{...props}
/>
)
}
function CardMeta({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="card-meta"
className={cn(
"text-label font-normal text-muted-foreground in-data-[tone=widget]:text-neutral-300",
className
)}
{...props}
/>
)
}
function CardBody({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="card-body"
className={cn(
"text-body text-pretty text-muted-foreground in-data-[tone=widget]:text-neutral-300",
className
)}
{...props}
/>
)
}
export {
Card,
CardBody,
CardMeta,
CardSpacer,
CardTitle,
CardWell,
cardVariants,
}