Card
Um componente container com layout estruturado para exibir conteúdo agrupado, como prévias, resumos ou itens de lista.
Busque em todas as páginas da documentação
Um componente container com layout estruturado para exibir conteúdo agrupado, como prévias, resumos ou itens de lista.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
interface CardProps {
title: string;
children: React.ReactNode;
}
export function Card({ title, children }: CardProps) {
return (
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<div className="mt-2 text-sm text-gray-600">{children}</div>
</div>
);
}Um container estático que não precisa de interatividade do cliente, portanto "use client" é omitido. O card usa uma borda sutil e sombra para separá-lo visualmente do fundo.
import Image from "next/image";
interface ImageCardProps {
src: string;
alt: string;
title: string;
children: React.ReactNode;
}
export function ImageCard({ src, alt, title, children }: ImageCardProps) {
return (
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<div className="relative h-48 w-full">
<Image src={src} alt={alt} fill className="object-cover" />
</div>
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<div className="mt-2 text-sm text-gray-600">{children}</div>
</div>
</div>
);
}Usa o Image do Next.js com fill e object-cover para lidar com o dimensionamento responsivo de imagens sem quebra de layout. O container da imagem tem altura fixa para que os cards em uma grade permaneçam alinhados.
import Image from "next/image";
interface HorizontalCardProps {
src: string;
alt: string;
title: string;
description: string;
}
export function HorizontalCard({ src, alt, title, description }: HorizontalCardProps) {
return (
<div className="flex overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<div className="relative h-auto w-48 shrink-0">
<Image src={src} alt={alt} fill className="object-cover" />
</div>
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-2 text-sm text-gray-600">{description}</p>
</div>
</div>
);
}Um layout lado a lado com a imagem à esquerda. O shrink-0 no container da imagem evita que ele encolha quando o conteúdo de texto é longo.
"use client";
import Link from "next/link";
interface ClickableCardProps {
href: string;
title: string;
description: string;
}
export function ClickableCard({ href, title, description }: ClickableCardProps) {
return (
<Link
href={href}
className="block rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-300 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-2 text-sm text-gray-600">{description}</p>
</Link>
);
}Envolver todo o card em um <Link> torna toda a superfície clicável. O hover:shadow-md e hover:border-blue-300 fornecem feedback visual claro de que o card é interativo.
type Status = "active" | "inactive" | "pending";
interface StatusCardProps {
title: string;
description: string;
status: Status;
}
const statusClasses: Record<Status, string> = {
active: "bg-green-100 text-green-700",
inactive: "bg-gray-100 text-gray-700",
pending: "bg-yellow-100 text-yellow-700",
};
const statusLabels: Record<Status, string> = {
active: "Active",
inactive: "Inactive",
pending: "Pending",
};
export function StatusCard({ title, description, status }: StatusCardProps) {
return (
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusClasses[status]}`}>
{statusLabels[status]}
</span>
</div>
<p className="mt-2 text-sm text-gray-600">{description}</p>
</div>
);
}Usa um Record para mapear valores de status a classes Tailwind e rótulos de exibição. O selo fica no cabeçalho do card usando justify-between para um alinhamento limpo.
interface CardGridProps {
children: React.ReactNode;
columns?: 2 | 3 | 4;
}
const columnClasses: Record<number, string> = {
2: "grid-cols-1 sm:grid-cols-2",
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
};
export function CardGrid({ children, columns = 3 }: CardGridProps) {
return (
<div className={`grid gap-6 ${columnClasses[columns]}`}>
{children}
</div>
);
}Um wrapper de layout que organiza os cards em uma grade responsiva. As colunas colapsam para uma única coluna no celular e depois se expandem nos breakpoints. O gap-6 mantém o espaçamento consistente entre os cards.
"use client";
interface ActionCardProps {
title: string;
description: string;
onPrimary: () => void;
onSecondary?: () => void;
primaryLabel: string;
secondaryLabel?: string;
}
export function ActionCard({
title, description, onPrimary, onSecondary, primaryLabel, secondaryLabel,
}: ActionCardProps) {
return (
<div className="flex flex-col rounded-xl border border-gray-200 bg-white shadow-sm">
<div className="flex-1 p-6">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-2 text-sm text-gray-600">{description}</p>
</div>
<div className="flex justify-end gap-3 border-t px-6 py-4">
{onSecondary && secondaryLabel && (
<button
onClick={onSecondary}
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
>
{secondaryLabel}
</button>
)}
<button
onClick={onPrimary}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
{primaryLabel}
</button>
</div>
</div>
);
}Usa flex-col com flex-1 no corpo para que o rodapé fique sempre na parte inferior, mesmo quando os cards em uma grade têm alturas de conteúdo diferentes.
"use client";
import { forwardRef, createContext, useContext } from "react";
import Image from "next/image";
import Link from "next/link";
// --- Contexto ---
interface CardContextValue {
interactive: boolean;
}
const CardContext = createContext<CardContextValue>({ interactive: false });
// --- Raiz ---
type CardRootAsDiv = React.HTMLAttributes<HTMLDivElement> & {
href?: never;
variant?: "elevated" | "outlined" | "filled";
children: React.ReactNode;
};
type CardRootAsLink = React.ComponentPropsWithoutRef<typeof Link> & {
href: string;
variant?: "elevated" | "outlined" | "filled";
children: React.ReactNode;
};
type CardRootProps = CardRootAsDiv | CardRootAsLink;
const variantClasses: Record<string, string> = {
elevated: "border border-gray-200 bg-white shadow-sm hover:shadow-md",
outlined: "border border-gray-200 bg-white",
filled: "bg-gray-50",
};
export const CardRoot = forwardRef<HTMLDivElement | HTMLAnchorElement, CardRootProps>(
function CardRoot(props, ref) {
const { variant = "elevated", children, className, ...rest } = props;
const base = `rounded-xl transition-all ${variantClasses[variant]} ${className ?? ""}`;
const isLink = "href" in rest && rest.href;
if (isLink) {
const { href, ...linkRest } = rest as CardRootAsLink;
return (
<CardContext.Provider value={{ interactive: true }}>
<Link
ref={ref as React.Ref<HTMLAnchorElement>}
href={href}
className={`block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${base}`}
{...linkRest}
>
{children}
</Link>
</CardContext.Provider>
);
}
return (
<CardContext.Provider value={{ interactive: false }}>
<div ref={ref as React.Ref<HTMLDivElement>} className={base} {...(rest as CardRootAsDiv)}>
{children}
</div>
</CardContext.Provider>
);
}
);
// --- Partes Compostas ---
export function CardImage({ src, alt, height = "h-48" }: { src: string; alt: string; height?: string }) {
return (
<div className={`relative ${height} w-full overflow-hidden rounded-t-xl`}>
<Image src={src} alt={alt} fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" />
</div>
);
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="px-6 pt-6">{children}</div>;
}
export function CardTitle({ children }: { children: React.ReactNode }) {
const { interactive } = useContext(CardContext);
return (
<h3 className={`text-lg font-semibold text-gray-900 ${interactive ? "group-hover:text-blue-600" : ""}`}>
{children}
</h3>
);
}
export function CardBody({ children }: { children: React.ReactNode }) {
return <div className="px-6 py-4 text-sm text-gray-600">{children}</div>;
}
export function CardFooter({ children }: { children: React.ReactNode }) {
return <div className="flex items-center gap-3 border-t px-6 py-4">{children}</div>;
}
type BadgeColor = "gray" | "green" | "red" | "yellow" | "blue";
const badgeColors: Record<BadgeColor, string> = {
gray: "bg-gray-100 text-gray-700",
green: "bg-green-100 text-green-700",
red: "bg-red-100 text-red-700",
yellow: "bg-yellow-100 text-yellow-700",
blue: "bg-blue-100 text-blue-700",
};
export function CardBadge({ children, color = "gray" }: { children: React.ReactNode; color?: BadgeColor }) {
return (
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${badgeColors[color]}`}>
{children}
</span>
);
}
// --- Exemplo de Uso ---
// <CardRoot href="/products/1" variant="elevated">
// <CardImage src="/product.jpg" alt="Product" />
// <CardHeader>
// <div className="flex items-center justify-between">
// <CardTitle>Product Name</CardTitle>
// <CardBadge color="green">In Stock</CardBadge>
// </div>
// </CardHeader>
// <CardBody>A short product description goes here.</CardBody>
// <CardFooter>
// <span className="text-lg font-bold text-gray-900">$49.99</span>
// </CardFooter>
// </CardRoot>Aspectos Chave:
CardRoot, CardImage, CardHeader, CardTitle, CardBody, CardFooter e CardBadge se compõem livremente. O pai nunca precisa saber quais subcomponentes estão sendo usados.href alterna entre <div> e <Link>. TypeScript impõe as props corretas para cada um.CardTitle lê o contexto para aplicar a cor de hover apenas quando o card é um link, evitando pistas interativas enganosas em cards estáticos.sizes - a dica sizes no <Image> informa ao navegador qual tamanho de imagem baixar em cada largura de viewport, evitando downloads excessivos no celular.elevated, outlined e filled cobrem necessidades visuais comuns sem substituições de classe personalizadas.Falta de overflow-hidden em cards com imagens - Sem ele, os cantos da imagem transbordam do border-radius do card. Sempre adicione overflow-hidden ao card ou ao container da imagem.
Alturas de card inconsistentes em uma grade - Cards com comprimentos de conteúdo variados parecem desorganizados. Use flex flex-col no card e flex-1 no corpo para que os rodapés se alinhem na parte inferior.
Card clicável com elementos interativos aninhados - Um card <Link> contendo um <button> aciona o link quando o botão é clicado. Use e.stopPropagation() no botão aninhado ou reestruture para que o link cubra apenas o título.
<Image> do Next.js sem a prop sizes - A prop fill gera um srcset, mas sem sizes, o navegador assume que a imagem é 100vw e baixa a maior versão. Sempre forneça uma dica sizes.
Conteúdo do card transbordando - Títulos ou descrições longas sem line-clamp quebram o layout. Use line-clamp-2 ou truncate nos elementos de texto para impor um comprimento máximo visível.
Usar <div> em vez de <article> para cards de conteúdo - Se um card representa um conteúdo independente (post de blog, produto), use <article> para melhor semântica e acessibilidade.
Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥