Przejdź do treści

Card

Wyniesiona powierzchnia, która grupuje powiązane treści i akcje dotyczące jednego tematu.

Kod źródłowyRejestr
Miejsce na dane
x
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

Miejsca w zespole
x
  1. 1KontenerWyniesiona powierzchnia z cienkim obramowaniem, która mieści jeden temat. Jej rozmiar wyznacza padding i odstępy między częściami.
  2. 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.
  3. 3TytułNazywa temat. Opis pod nim dodaje kontekst.
  4. 4TreśćGłówna część — cokolwiek, z paddingiem wyrównanym do nagłówka i stopki.
  5. 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.json

Uż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.

Domyślny
Do głównych treści: formularzy, podsumowań, grup ustawień.
Mały
Do gęstych pulpitów i siatek kafelków.
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.

Członkowie zespołu
MC
Maya Chenmaya@northwind.dev
Właściciel
JO
Jonah Okaforjonah@northwind.dev
Administrator
LP
Lena Parklena@northwind.dev
Członek
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.

48 210 $+12,4%
względem poprzedniego miesiąca
8 924+3,1%
względem poprzedniego miesiąca
1,8%+0,3 p.p.
względem poprzedniego miesiąca
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

Team
Najpopularniejszy

24 $za użytkownika / miesiąc

  • Nielimitowane projekty
  • Do 20 współpracowników
  • Dziennik audytu i SSO
  • Priorytetowe wsparcie, odpowiedź w ciągu 4 godz.
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.

Powiadomienia

Zmiany dotyczą wszystkich projektów.

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.

KonferencjaHybrydowa
Systems & Craft 2026
14–15 października 2026Kraków i online
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

Przychód
Użytkownicy
DobrzeKażda karta mieści jeden temat, a grupowaniem zajmuje się układ strony.
Przychód
ŹleKarty zagnieżdżone w kartach piętrzą obramowania i cienie, aż nic nie wygląda na wyniesione.

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ą. CardTitle renderuje <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. CardAction jest 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="" lub aria-hidden.

Dokumentacja API

Card

<div> z wyniesioną powierzchnią, obramowaniem i wspólnymi odstępami.

PropTypDomyślnie
size

Padding i odstęp między sekcjami: 24/20 px albo 16/12 px.

"default" | "sm""default"

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.