Primitives
Card
A panel with a border that groups the content and the actions of one topic.
Primitives
A panel with a border that groups the content and the actions of one topic.
import { Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ferry-ui'
export default function CardHero() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<div>
<CardTitle>Payment method</CardTitle>
<CardDescription>We charge this card on the first day of each month.</CardDescription>
</div>
<CardAction>
<Button size="tiny">Replace</Button>
</CardAction>
</CardHeader>
<CardContent className="text-sm">Card ending in 4242</CardContent>
<CardFooter className="justify-between text-[13px] text-foreground-light">Next charge on Nov 1</CardFooter>
</Card>
)
}Import the parts and put them together. Each part in the card is optional.
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ferry-ui'
<Card>
<CardHeader>
<div>
<CardTitle />
<CardDescription />
</div>
<CardAction />
</CardHeader>
<CardContent />
<CardFooter />
</Card>| Part | Role |
|---|---|
Card | The panel with a border. |
CardHeader | The top bar. It puts the title on the left and the action on the right. |
CardTitle | The title of the card. |
CardDescription | One short sentence about the card. |
CardAction | Small controls on the right of the header. |
CardContent | The body, with padding. |
CardFooter | The bottom bar. It aligns its content to the right. |
Wrap CardTitle and CardDescription in a <div>. The description then shows below the title.
A card needs no header and no footer. CardContent alone gives a panel with padding.
API keys give your backend access to the workspace. Keep them out of your source code.
import { Card, CardContent } from 'ferry-ui'
export default function CardContentOnly() {
return (
<Card className="w-full max-w-md">
<CardContent>
<p className="text-sm text-foreground-light">
API keys give your backend access to the workspace. Keep them out of your source code.
</p>
</CardContent>
</Card>
)
}CardAction holds small controls on the right of the header: a button, a menu or a Badge. The buttons of this demo have the tiny and icon-tiny sizes.
Keys give programs access to your workspace.
import { Button, Card, CardAction, CardContent, CardHeader, CardTitle } from 'ferry-ui'
import { MoreHorizontal, Plus } from 'lucide-react'
export default function CardHeaderAction() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>API keys</CardTitle>
<CardAction>
<Button size="tiny" icon={<Plus />}>
New key
</Button>
<Button variant="ghost" size="icon-tiny" icon={<MoreHorizontal />} aria-label="More actions" />
</CardAction>
</CardHeader>
<CardContent>
<p className="text-sm text-foreground-light">Keys give programs access to your workspace.</p>
</CardContent>
</Card>
)
}CardFooter aligns its content to the right. Add justify-between to put helper text on the left.
import { Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ferry-ui'
import { ArrowUpRight } from 'lucide-react'
export default function CardWithFooter() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<div>
<CardTitle>Usage this month</CardTitle>
<CardDescription>The count starts again on October 1.</CardDescription>
</div>
<CardAction>
<Badge variant="warning">82%</Badge>
</CardAction>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-1">
<span className="tabular text-2xl font-medium text-foreground">8,214</span>
<span className="text-sm text-foreground-lighter">/ 10,000 requests</span>
</div>
</CardContent>
<CardFooter className="justify-between">
<span className="text-[13px] text-foreground-lighter">A higher plan has higher limits.</span>
<Button iconRight={<ArrowUpRight />}>View plans</Button>
</CardFooter>
</Card>
)
}Leave out CardContent for content that must touch the edges, such as a Table. Remove the frame of the table with containerClassName.
| Order | Customer | Status | Total |
|---|---|---|---|
| #1042 | Northwind Traders | Paid | $1,240.00 |
| #1043 | Acme | Pending | $860.50 |
| #1044 | Globex | Refunded | $3,105.00 |
import {
Button,
Card,
CardAction,
CardHeader,
CardTitle,
StatusBadge,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
type StatusTone,
} from 'ferry-ui'
const ORDERS: { id: string; customer: string; total: string; tone: StatusTone; status: string }[] = [
{ id: '#1042', customer: 'Northwind Traders', total: '$1,240.00', tone: 'success', status: 'Paid' },
{ id: '#1043', customer: 'Acme', total: '$860.50', tone: 'info', status: 'Pending' },
{ id: '#1044', customer: 'Globex', total: '$3,105.00', tone: 'neutral', status: 'Refunded' },
]
export default function CardWithTable() {
return (
<Card className="mx-auto w-full max-w-xl">
<CardHeader>
<CardTitle>Recent orders</CardTitle>
<CardAction>
<Button size="tiny">View all</Button>
</CardAction>
</CardHeader>
{/* No CardContent: the table touches the edges. Its own frame is off. */}
<Table aria-label="Recent orders" containerClassName="rounded-none border-0 shadow-none">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Order</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ORDERS.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-mono text-[13px]">{order.id}</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell>
<StatusBadge tone={order.tone} label={order.status} />
</TableCell>
<TableCell className="text-right tabular">{order.total}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)
}A long title wraps by default. To keep it on one line, give min-w-0 to its wrapper and truncate to CardTitle.
The text of the body wraps on more than one line.
import { Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from 'ferry-ui'
export default function CardLongTitle() {
return (
<Card className="w-full max-w-xs">
<CardHeader>
{/* min-w-0 lets the title block get smaller than its text. */}
<div className="min-w-0">
<CardTitle className="truncate">Plan for the customer support and the sales teams</CardTitle>
<CardDescription className="truncate">Last edit by Maya Chen, three days ago</CardDescription>
</div>
<CardAction>
<Button size="tiny">Open</Button>
</CardAction>
</CardHeader>
<CardContent>
<p className="text-sm text-foreground-light">The text of the body wraps on more than one line.</p>
</CardContent>
</Card>
)
}While the content loads, show a Skeleton for each line. Set aria-busy on the card.
import { Card, CardContent, CardHeader, Skeleton } from 'ferry-ui'
export default function CardLoading() {
return (
<Card className="w-full max-w-md" aria-busy="true">
<CardHeader>
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3.5 w-48" />
</div>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<Skeleton className="h-3.5 w-full" />
<Skeleton className="h-3.5 w-full" />
<Skeleton className="h-3.5 w-2/3" />
</CardContent>
</Card>
)
}CardTitle is a <div>. If the outline of the page needs a heading, put an <h2> or an <h3> in it.Each part accepts the attributes of the <div> element.
Card has no props of its own. It accepts the attributes of the element it renders.
CardHeader has no props of its own. It accepts the attributes of the element it renders.
CardTitle has no props of its own. It accepts the attributes of the element it renders.
CardDescription has no props of its own. It accepts the attributes of the element it renders.
CardAction has no props of its own. It accepts the attributes of the element it renders.
CardContent has no props of its own. It accepts the attributes of the element it renders.
CardFooter has no props of its own. It accepts the attributes of the element it renders.