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>
  )
}