Card
Wyniesiona powierzchnia, która grupuje powiązane treści i akcje dotyczące jednego tematu.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
export default function CardDemo() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Miejsce na dane</CardTitle>
<CardDescription>Wykorzystano 36,2 GB z 50 GB</CardDescription>
</CardHeader>
<CardContent>
<Progress value={72} aria-label="Wykorzystane miejsce na dane" />
</CardContent>
<CardFooter className="justify-end">
<Button size="sm">Przejdź na wyższy plan</Button>
</CardFooter>
</Card>
)
}Anatomia
- 1KontenerWyniesiona powierzchnia z cienkim obramowaniem, która mieści jeden temat. Jej rozmiar wyznacza padding i odstępy między częściami.
- 2StopkaAkcje dotyczące całej karty, na spokojnym pasie pod cienką linią. Zostaje na dole, gdy karty w rzędzie rozciągają się do równej wysokości.
- 3TytułNazywa temat. Opis pod nim dodaje kontekst.
- 4TreśćGłówna część — cokolwiek, z paddingiem wyrównanym do nagłówka i stopki.
- 5AkcjaOpcjonalna kontrolka w górnym rogu nagłówka, wyrównana z tytułem.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/card.jsonUżycie
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"<Card>
<CardHeader>
<CardTitle>Storage</CardTitle>
<CardDescription>36.2 GB of 50 GB used</CardDescription>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>…</CardFooter>
</Card>Używaj pełnej kompozycji. Części korzystają ze wspólnej zmiennej odstępu, więc nagłówki, treść i stopki wyrównują się bez dodatkowych klas.
Przykłady
Rozmiary
default ma 24 px paddingu i 20 px odstępu między sekcjami. sm zmniejsza te wartości do 16 px i 12 px — na potrzeby pulpitów i gęstych siatek. Każda część czyta tę samą zmienną --card-spacing, więc nigdy nie musisz nadpisywać paddingu w poszczególnych sekcjach.
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export default function CardSizes() {
return (
<div className="grid w-full max-w-2xl items-start gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Domyślny</CardTitle>
<CardDescription>
Odstęp wewnętrzny 24 px, między sekcjami 20 px.
</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground">
Do głównych treści: formularzy, podsumowań, grup ustawień.
</CardContent>
</Card>
<Card size="sm">
<CardHeader>
<CardTitle>Mały</CardTitle>
<CardDescription>
Odstęp wewnętrzny 16 px, między sekcjami 12 px.
</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground">
Do gęstych pulpitów i siatek kafelków.
</CardContent>
</Card>
</div>
)
}Z akcją
CardAction umieszcza kontrolki w prawym górnym rogu nagłówka, a tytuł i opis trzyma wyrównane do lewej, niezależnie od ich długości. Gdy brakuje miejsca, najpierw zawija się tytuł, a akcja zostaje na miejscu; akcje w stopce przechodzą do drugiego wiersza, więc na telefonie nic nie zostaje ucięte.
import { MoreHorizontalIcon, UserPlusIcon } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const members = [
{
name: "Maya Chen",
email: "maya@northwind.dev",
role: "Właściciel",
owner: true,
},
{ name: "Jonah Okafor", email: "jonah@northwind.dev", role: "Administrator" },
{ name: "Lena Park", email: "lena@northwind.dev", role: "Członek" },
]
export default function CardWithAction() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Członkowie zespołu</CardTitle>
<CardDescription>Wykorzystano 3 z 5 licencji.</CardDescription>
<CardAction className="flex gap-1">
<Button size="sm" variant="outline">
<UserPlusIcon data-icon="inline-start" />
Zaproś
</Button>
<Button size="icon-sm" variant="ghost" aria-label="Więcej opcji">
<MoreHorizontalIcon />
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{members.map((member) => (
<div key={member.email} className="flex items-center gap-3">
<Avatar>
<AvatarFallback>
{member.name
.split(" ")
.map((part) => part[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{member.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{member.email}
</span>
</div>
<Badge variant={member.owner ? "brand" : "secondary"}>
{member.role}
</Badge>
</div>
))}
</CardContent>
</Card>
)
}Statystyka
Karty to naturalne miejsce na kluczowe liczby. Etykieta powinna być mała i stonowana, wartość duża, a przy każdej zmianie podaj okres porównawczy.
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card"
const stats = [
{
label: "Przychody miesięczne",
value: "48 210 $",
delta: "+12,4%",
up: true,
good: true,
},
{
label: "Aktywni użytkownicy",
value: "8 924",
delta: "+3,1%",
up: true,
good: true,
},
{
label: "Wskaźnik rezygnacji",
value: "1,8%",
delta: "+0,3 p.p.",
up: true,
good: false,
},
]
export default function CardStat() {
return (
<div className="grid w-full max-w-3xl gap-4 sm:grid-cols-3">
{stats.map((stat) => {
const Icon = stat.up ? ArrowUpRightIcon : ArrowDownRightIcon
return (
<Card key={stat.label} size="sm">
<CardHeader>
<CardDescription>{stat.label}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-1.5">
<div className="flex items-center justify-between gap-2">
{/* Proportional figures: tabular digits look loose at display size. */}
<span className="text-heading-lg">{stat.value}</span>
<Badge variant={stat.good ? "success" : "destructive"}>
<Icon data-icon="inline-start" />
{stat.delta}
</Badge>
</div>
<span className="text-xs text-muted-foreground">
względem poprzedniego miesiąca
</span>
</CardContent>
</Card>
)
})}
</div>
)
}Cennik
import { CheckIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const features = [
"Nielimitowane projekty",
"Do 20 współpracowników",
"Dziennik audytu i SSO",
"Priorytetowe wsparcie, odpowiedź w ciągu 4 godz.",
]
export default function CardPricing() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Team</CardTitle>
<CardDescription>
Dla zespołów produktowych, które wdrażają co tydzień.
</CardDescription>
<CardAction>
<Badge variant="brand">Najpopularniejszy</Badge>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<p className="flex items-baseline gap-1">
<span className="text-display-sm">24 $</span>
<span className="text-sm text-muted-foreground">
za użytkownika / miesiąc
</span>
</p>
<ul className="flex flex-col gap-2.5">
{features.map((feature) => (
<li key={feature} className="flex items-center gap-2.5 text-sm">
<CheckIcon
className="size-4 shrink-0 text-brand-11"
aria-hidden="true"
/>
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Rozpocznij 14-dniowy okres próbny</Button>
</CardFooter>
</Card>
)
}Ustawienia
Nagłówek z obramowaniem i zabarwiony pasek CardFooter tworzą ramę dla grupy ustawień. W stopce jest miejsce na akcję zatwierdzającą kartę — oddzieloną od kontrolek, których zmiany zapisuje.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function CardSettings() {
return (
<Card className="w-full max-w-lg">
<CardHeader className="border-b">
<CardTitle>Powiadomienia</CardTitle>
<CardDescription>Wybierz, o czym mamy cię powiadamiać e-mailem.</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="deploys">Wdrożenia</FieldLabel>
<FieldDescription>
Gdy wdrożenie produkcyjne się powiedzie lub zakończy błędem.
</FieldDescription>
</FieldContent>
<Switch id="deploys" defaultChecked />
</Field>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="comments">Komentarze</FieldLabel>
<FieldDescription>
Gdy ktoś oznaczy cię w recenzji.
</FieldDescription>
</FieldContent>
<Switch id="comments" defaultChecked />
</Field>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="digest">Cotygodniowe podsumowanie</FieldLabel>
<FieldDescription>
Poniedziałkowe podsumowanie wykorzystania i wydatków.
</FieldDescription>
</FieldContent>
<Switch id="digest" />
</Field>
</FieldGroup>
</CardContent>
<CardFooter className="justify-between">
<p className="text-xs text-muted-foreground">
Zmiany dotyczą wszystkich projektów.
</p>
<Button size="sm">Zapisz preferencje</Button>
</CardFooter>
</Card>
)
}Z obrazem
Wstaw <img> jako pierwszy element potomny, a karta usunie górny padding i zaokrągli obraz zgodnie ze swoimi narożnikami. Dekoracyjne okładki dostają pusty alt.
import { CalendarIcon, MapPinIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import cover from "@/examples/card/assets/conference-cover.svg"
export default function CardImage() {
return (
<Card className="w-full max-w-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={cover.src}
alt=""
width={640}
height={360}
className="aspect-[16/9] w-full object-cover"
/>
<CardHeader>
<div className="flex items-center gap-2">
<Badge variant="brand">Konferencja</Badge>
<Badge variant="outline">Hybrydowa</Badge>
</div>
<CardTitle className="text-heading-md">
Systems & Craft 2026
</CardTitle>
<CardDescription>
Dwa dni o design systemach, tokenach i rzemiośle inżynierii interfejsów.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2 text-sm text-muted-foreground">
<span className="flex items-center gap-2">
<CalendarIcon className="size-4" aria-hidden="true" />
14–15 października 2026
</span>
<span className="flex items-center gap-2">
<MapPinIcon className="size-4" aria-hidden="true" />
Kraków i online
</span>
</CardContent>
<CardFooter className="gap-2">
<Button className="flex-1">Kup bilety</Button>
<Button variant="outline">Program</Button>
</CardFooter>
</Card>
)
}Wytyczne
Kiedy używać
- Do grupowania treści i akcji dotyczących jednego tematu — projektu, planu, wskaźnika, sekcji ustawień.
- Do prezentowania w siatce kolekcji równorzędnych obiektów, które można szybko przejrzeć i porównać.
Kiedy nie używać
- Do wizualnego oddzielenia pojedynczego bloku treści na stronie — użyj odstępów albo komponentu Separator.
- Do wierszy jednorodnych danych — użyj komponentu Table albo listy elementów Item.
- Do treści, która wymaga uwagi od razu — użyj komponentu Alert albo Dialog.
Jeden temat, jedna karta
Akcje
- Główną akcję karty umieść w
CardFooter, wyrównaną do końca. Akcje drugorzędne idą przed nią. - Akcje dotyczące samego obiektu — Edytuj, Udostępnij, menu z dodatkowymi opcjami — umieść w
CardAction. - Jeśli cała karta prowadzi do innej strony, zrób linkiem tytuł i rozciągnij jego obszar kliknięcia na całą kartę, zamiast umieszczać interaktywne elementy wewnątrz linku.
Elewacja
Karty są na poziomie wyniesionym: cienkie obramowanie bez cienia, w obu trybach. Nie dodawaj cieni, żeby karta się wyróżniała — o wyróżnieniu decydują treść i położenie, nie głębia. Cienie pływające należą do popoverów i okien dialogowych.
Dostępność
- Nadaj nagłówek, gdy karta jest sekcją.
CardTitlerenderuje<div>, więc pasuje do każdej struktury dokumentu. Gdy karty są głównymi sekcjami strony, umieść w tytule nagłówek odpowiedniego poziomu —<CardTitle><h3>Storage</h3></CardTitle>— a przejmie on styl tytułu. - Zachowaj logiczną kolejność czytania.
CardActionjest wizualnie w rogu, ale w DOM występuje po tytule i opisie, więc czytnik ekranu najpierw poda, czego dotyczy karta, a dopiero potem, co można z nią zrobić. - Media potrzebują alternatyw. Obrazy niosące informację potrzebują tekstu
alt, a dekoracyjne —alt=""lubaria-hidden.
Dokumentacja API
Card
<div> z wyniesioną powierzchnią, obramowaniem i wspólnymi odstępami.
CardHeader
Siatka, która rozmieszcza tytuł, opis i opcjonalny CardAction. Dodaj className="border-b", żeby oddzielić nagłówek od treści; jego dolny padding dopasuje się automatycznie.
CardTitle
Tytuł karty, złożony stylem heading-sm (w małych kartach heading-xs).
CardDescription
Tekst pomocniczy pod tytułem, w kolorze muted-foreground.
CardAction
Kontrolki przypięte do prawego górnego rogu nagłówka, rozciągnięte na wysokość wierszy tytułu i opisu.
CardContent
Główna treść z poziomym paddingiem dopasowanym do nagłówka.
CardFooter
Zabarwiony pasek oddzielony obramowaniem — na akcje i status karty. Usuwa dolny padding karty, żeby tło sięgało krawędzi.