Skip to content

Card

Group related content and actions on a card, with optional lift or lighting effects.

import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use Card to group content and actions for a project, integration or usage summary. Use headings and spacing for sections that do not need a separate surface. Reserve lift and spotlight for cards that are themselves interactive.

import { Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@youj/react'

export function UsageCard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Usage this cycle</CardTitle>
        <CardDescription>Resets in 12 days, on October 1.</CardDescription>
        <CardAction>
          <Button variant="outline" size="sm">
            Details
          </Button>
        </CardAction>
      </CardHeader>
      <CardContent>{/* meters, a chart, a list */}</CardContent>
      <CardFooter>
        <Button size="sm">Upgrade</Button>
      </CardFooter>
    </Card>
  )
}

CardHeader sets the title and description in a grid. A CardAction inside it takes its own column at the end and spans both rows, so a long title wraps without pushing the action down. CardContent shares the header's inset. CardFooter is recessed onto the muted surface, with a hairline above it and the card's inner radius below. An image as the card's first child runs edge to edge, with its top corners rounded to match.

Cards of equal height

A card is a flex column, and grid items stretch to the row's height, so cards in a grid are already as tall as the tallest. To keep the footers on one line, push each footer to the bottom with mt-auto. If a wrapper sits between the grid and the card, such as a link, give the wrapper h-full and the Card h-full too:

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
  {plans.map((plan) => (
    <Card key={plan.name} className="h-full">
      <CardHeader>
        <CardTitle>{plan.name}</CardTitle>
        <CardDescription>{plan.summary}</CardDescription>
      </CardHeader>
      <CardContent>{plan.features}</CardContent>
      <CardFooter className="mt-auto">
        <Button className="w-full">Choose {plan.name}</Button>
      </CardFooter>
    </Card>
  ))}
</div>

Mark the card to look at first, such as the recommended plan, with a ring in the accent color. A Badge in the header names why:

<Card className="ring-2 ring-accent">
  <CardHeader>
    <CardTitle>Pro</CardTitle>
    <CardAction>
      <Badge>Most popular</Badge>
    </CardAction>
  </CardHeader>
  {/* … */}
</Card>

With a form

To submit what a card holds, wrap its parts in a <form> inside the card, so the footer's button submits it:

import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Field, FieldLabel, Input } from '@youj/react'

export function ProjectNameCard({ onSave }: { onSave: (name: string) => void }) {
  return (
    <Card>
      <form
        onSubmit={(event) => {
          event.preventDefault()
          onSave(String(new FormData(event.currentTarget).get('name')))
        }}
      >
        <CardHeader>
          <CardTitle>Project name</CardTitle>
          <CardDescription>Shown in the sidebar and in links.</CardDescription>
        </CardHeader>
        <CardContent>
          <Field>
            <FieldLabel htmlFor="project-name">Name</FieldLabel>
            <Input id="project-name" name="name" defaultValue="Orion" />
          </Field>
        </CardContent>
        <CardFooter>
          <Button type="submit" size="sm">
            Save
          </Button>
        </CardFooter>
      </form>
    </Card>
  )
}

The card lays out a <form> that is its direct child as if it weren't there (display: contents), so the parts keep the card's spacing and Bezel's header band, dividers and section shading. Without that, the form would be the card's only child: the gap between parts would go, and Bezel's rules, which style the card's own children, would miss the header and footer. Put the form directly in the card, around the parts. A form nested deeper, or another wrapper element, isn't looked through. Card has no render prop, so it can't render as the form itself.

Reference

Propssize, variant, lift… (4)

Props

Card props
PropType
size'default' | 'sm'

default 'default'

'default'
variant'default' | 'inset' | 'panel'

inset: in the Bezel style, a shell carrying the head, with the rest in a well inside it at the concentric radius. Classic draws it as the default card. panel: in every style, a slim header strip (the title as a small label, an info tip, a menu button) over the content on a raised surface inset in the shell.

default 'default'

'default'
liftboolean

Rise on hover and focus: for cards that are one big target.

default false

false
spotlightboolean

Light the card with motion's Spotlight effect.

default false

false
Other props pass through: extends ComponentProps<'div'>.

CardHeader, CardTitle, CardDescription, CardAction, CardContent and CardFooter take the props of a div.

MotionLift, press, spotlight in and out… · reduced motion

Motion

Motion tokens
MomentTokens
Liftrise 2px, deeper shadow, on hover and focus
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Presssets the card back down
duration.instantease.standard

80ms · cubic-bezier(0.2, 0, 0, 1)

80mscubic-bezier(0.2, 0, 0, 1)
Spotlight in and outthe glow and the lit edge fade
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Spotlight trackingthe light follows the pointer
no token

every animation frame

every animation frame
Focus glidethe light moves to the focused card
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

Lift drops the rise and changes the shadow and border at once. Spotlight stops tracking and shows a static accent border on hover and focus.

The light is a radial gradient moved with transform, and the lit edge is the same gradient masked to a 1px ring, so tracking never triggers layout.

Reduced motion

A lifting card stays in place. Its shadow and border still change on hover and focus, at once. A spotlight card doesn't track the pointer. Hovering or focusing it shows a static accent border instead, so it still answers.

AccessibilityKeyboard, ARIA

Accessibility

  • A card is a div with no role. Give a set of cards list semantics by rendering them in a ul, one card per li.
  • CardTitle is a div, so it doesn't appear in the page's outline. When a card titles a section, put a heading inside it, such as <CardTitle><h2>Usage this cycle</h2></CardTitle>. The heading keeps the card's type.
  • A lifting card needs one real target. A stretched link gives it a name, link semantics and a single tab stop. Keep other controls out of a stretched-link card, or lift them above the link's overlay with relative z-10.
  • Spotlight's light layers are aria-hidden. The card's content and tab order don't change.