Cards
Cards display content and actions about a single subject
Usage
Cards are used to display content and actions on a single topic.
Instalation
Run the following command if is not already installed:
npm i @radix-ui/react-slot
Copy and paste the following code into your project.
import React from 'react'
import { Slot, Slottable } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const cardVariants = cva(
'transition w-full relative rounded-md overflow-hidden pb-4',
{
variants: {
variant: {
elevated: 'bg-surfaceContainerLow shadow-sm',
filled: 'bg-surfaceContainerHigh',
outlined: 'bg-surface border border-outlineVariant',
},
},
defaultVariants: {
variant: 'filled',
},
}
)
const actionCardVariants = cva('group outline-none z-0', {
variants: {
variant: {
elevated: 'hover:shadow-md aria-disabled:bg-surfaceVariant',
filled: 'hover:shadow-sm',
outlined: 'aria-disabled:border-outline/70',
},
},
defaultVariants: {
variant: 'filled',
},
})
export interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
const CardRoot = React.forwardRef<HTMLDivElement, CardProps>(
({ variant, className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(cardVariants({ variant, className }))}
{...props}
/>
)
}
)
CardRoot.displayName = 'CardRoot'
const ActionCardRoot = React.forwardRef<
HTMLDivElement,
CardProps & { disabled?: boolean }
>(({ variant, className, disabled, children, ...props }, ref) => {
return (
<Slot
ref={ref}
aria-disabled={disabled}
className={cn(
cardVariants({ variant, className }),
actionCardVariants({ variant, className }),
disabled &&
'disabled pointer-events-none opacity-38 shadow-none hover:shadow-none'
)}
{...props}
>
<Slottable>{children}</Slottable>
<span className="absolute inset-0 z-[-1] transition-colors group-hover:bg-onSurface/4 group-focus:bg-onSurface/8 group-active:bg-onSurface/8" />
</Slot>
)
})
ActionCardRoot.displayName = 'ActionCard'
const CardThumbnail = React.forwardRef<
HTMLImageElement,
React.ImgHTMLAttributes<HTMLImageElement>
>(({ className, ...props }, ref) => (
<img
ref={ref}
className={cn('h-full w-full rounded-b-md object-cover', className)}
{...props}
/>
))
CardThumbnail.displayName = 'CardThumbnail'
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex flex-col space-y-1.5 p-4 pb-0 text-onSurface',
className
)}
{...props}
/>
))
CardHeader.displayName = 'CardHeader'
const CardHeadline = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('text-headline-md', className)} {...props} />
))
CardHeadline.displayName = 'CardHeadline'
const CardSubhead = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-body-lg text-onSurfaceVariant', className)}
{...props}
/>
))
CardSubhead.displayName = 'CardSubhead'
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4 pb-0 pt-2 text-body-md text-onSurface/70', className)}
{...props}
/>
))
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-4 pb-0', className)}
{...props}
/>
))
CardFooter.displayName = 'CardFooter'
const ActionCard = Object.assign(ActionCardRoot, {
Header: CardHeader,
Thumbnail: CardThumbnail,
Headline: CardHeadline,
Subhead: CardSubhead,
Content: CardContent,
Footer: CardFooter,
})
const Card = Object.assign(CardRoot, {
Header: CardHeader,
Thumbnail: CardThumbnail,
Headline: CardHeadline,
Subhead: CardSubhead,
Content: CardContent,
Footer: CardFooter,
})
export {
Card,
ActionCard,
CardRoot,
CardHeader,
CardHeadline,
CardSubhead,
CardContent,
CardFooter,
cardVariants,
actionCardVariants,
}
Update the import paths to match your project setup.
Examples
Filled
Filled cards provide subtle separation from the background. This has less emphasis than elevated or outlined cards.
Glass Souls' World Tour
From your recent favorites
Glass Souls will deliver a show that will keep you on the edge of your seat.
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
export const ElevatedCard = () => {
return (
<Card className="max-w-[350px]">
<Card.Thumbnail src="./card_thumbnail" className="h-[200px]" />
<Card.Header>
<Card.Headline>Glass Souls' World Tour</Card.Headline>
<Card.Subhead>From your recent favorites</Card.Subhead>
</Card.Header>
<CardContent>
Glass Souls will deliver a show that will keep you on the edge of your
seat.
</CardContent>
<Card.Footer>
<Button>Buy Tickets</Button>
</Card.Footer>
</Card>
)
}
Elevated
Elevated cards have a drop shadow, providing more separation from the background than filled cards, but less than outlined cards.
Glass Souls' World Tour
From your recent favorites
Glass Souls will deliver a show that will keep you on the edge of your seat.
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
export const ElevatedCard = () => {
return (
<Card variant="elevated" className="max-w-[350px]">
<Card.Thumbnail src="./card_thumbnail" className="h-[200px]" />
<Card.Header>
<Card.Headline>Glass Souls' World Tour</Card.Headline>
<Card.Subhead>From your recent favorites</Card.Subhead>
</Card.Header>
<CardContent>
Glass Souls will deliver a show that will keep you on the edge of your
seat.
</CardContent>
<Card.Footer>
<Button>Buy Tickets</Button>
</Card.Footer>
</Card>
)
}
Outlined
Outlined cards have a visual boundary around the container. This can provide greater emphasis than the other types.
Glass Souls' World Tour
From your recent favorites
Glass Souls will deliver a show that will keep you on the edge of your seat.
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
export const ElevatedCard = () => {
return (
<Card variant="oulined" className="max-w-[350px]">
<Card.Thumbnail src="./card_thumbnail" className="h-[200px]" />
<Card.Header>
<Card.Headline>Glass Souls' World Tour</Card.Headline>
<Card.Subhead>From your recent favorites</Card.Subhead>
</Card.Header>
<CardContent>
Glass Souls will deliver a show that will keep you on the edge of your
seat.
</CardContent>
<Card.Footer>
<Button>Buy Tickets</Button>
</Card.Footer>
</Card>
)
}
Action Card
Action cards serve as entry points into deeper levels of detail or navigation.
import { Button } from '@/components/ui/button'
import { ActionCard, Card } from '@/components/ui/card'
export const ActionCardExample = () => {
return (
<ActionCard className="max-w-[350px]">
<a href="#action-card">
<Card.Thumbnail
src="https://lh3.googleusercontent.com/GC7uaJYBY37yDjlX_l1y8oXf_g1iNwiJDB4ulfbSx0QOsxee2ex3vg2HW-qyKCHFc8IyVh7KIofdCXR3pw3IRMrL1uMYtaMJKZ9Ou7yGfyWuLJvUPxpW=w2400-rj"
className="h-[100px]"
/>
<Card.Header>
<Card.Headline className="text-title-md">
Customizing Material
</Card.Headline>
<Card.Subhead className="text-label-lg">
Make it personal
</Card.Subhead>
</Card.Header>
</a>
</ActionCard>
)
}
Card with top actions
JC
Jepri Creations
Yesterday
Clay pot fair on Saturday?
I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone els...
import { Avatar } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { ActionCard, Card } from '@/components/ui/card'
import { Icon } from '@/components/ui/icon'
import { IconButton } from '@/components/ui/icon-button'
export const CardWithTopActions = () => {
const [isFav, setIsFav] = useState(false)
return (
<Card className="max-w-[300px]">
<Card.Header>
<div className="mb-2 flex items-start justify-between">
<div className="flex items-center gap-3">
<Avatar>
<Avatar.Image
src="https://github.com/jepricreations.png"
alt="Avatar"
/>
<Avatar.Fallback>JC</Avatar.Fallback>
</Avatar>
<div className="text-label-md">
<div className="text-onSurfaceVariant">Jepri Creations</div>
<div className="text-onSurfaceVariant/60">Yesterday</div>
</div>
</div>
<IconButton
variant="standard"
disableStateLayer
className="bg-background hover:bg-background/70"
onClick={() => setIsFav(!isFav)}
>
<Icon
symbol="star"
className={
isFav
? 'font-filled duration-100 ease-out'
: 'font-regular duration-0'
}
/>
</IconButton>
</div>
<Card.Headline className="text-title-md font-normal">
Clay pot fair on Saturday?
</Card.Headline>
</Card.Header>
<CardContent>
I think it's time for us to finally try that new noodle shop downtown
that doesn't use menus. Anyone els...
</CardContent>
</Card>
)
}