Gesso
Data display

Interactive card

A card of slots, stacked or side by side, that lifts, zooms, or reveals on hover and focus.

Interactive card is a framed surface you open. It is built from slots: media, an eyebrow, a title, a description, and a footer with meta and actions. orientation stacks the media on the text or places it at the start of the row. Card is the static section of a page, and this one is the thing you follow.

Usage

import {
  InteractiveCard,
  InteractiveCardContent,
  InteractiveCardDescription,
  InteractiveCardEyebrow,
  InteractiveCardFooter,
  InteractiveCardMedia,
  InteractiveCardMeta,
  InteractiveCardTitle,
} from '@/components/ui/interactive-card';

<InteractiveCard asChild>
  <a href="/notes/north-shore">
    <InteractiveCardMedia>
      <img src="/notes/north-shore.jpg" alt="" />
    </InteractiveCardMedia>
    <InteractiveCardContent>
      <InteractiveCardEyebrow>Field notes</InteractiveCardEyebrow>
      <InteractiveCardTitle>North shore survey</InteractiveCardTitle>
      <InteractiveCardDescription>
        The tide line moved twelve meters since the last visit.
      </InteractiveCardDescription>
      <InteractiveCardFooter>
        <InteractiveCardMeta>Updated today</InteractiveCardMeta>
      </InteractiveCardFooter>
    </InteractiveCardContent>
  </a>
</InteractiveCard>

Use the slots the content needs. The footer sits at the bottom of the text, so a short card and a tall one in the same row keep their meta aligned. A picture across the top is in Picture.

Orientation

vertical is the default: the media is a 16:10 frame across the top, and the text sits under it. horizontal places the media at the start, 9rem wide, and the text beside it, as long as the card is at least 20rem wide. Narrower than that, the text sits under the media and the media spans the card at its aspect, so a horizontal card can sit in a gallery column. When a grid stretches that card to the row, the picture stays at its aspect and the text takes the extra height, so the footer still lines up. Beside the text, the picture is as tall as that text and at least 8rem, and a stretched row does not lengthen it.

Picture

InteractiveCardMedia is the frame. Put an img in it and the picture fills that frame and crops. aspect is wide (16:10, the default), square, or tall (3:4). The sunken fill shows until the file has painted.

InteractiveCardMediaCaption is the line on the bottom of the picture. It stays put when the picture zooms. Use it for a line the title does not already say, such as the place. InteractiveCardMediaScrim is the dark fade behind that line, and InteractiveCardMediaCorner is the top right, for a control on the picture, such as saving the note. All three go in InteractiveCardMediaOverlay, which sits over the picture and does not zoom with it. Use the slots you need. The corner is its own control, so put the link on the title and leave the card itself a plain element. A button inside a card that is already a link is nested.

<InteractiveCardMedia aspect="wide" inset>
  <img src="/notes/north-shore.jpg" alt="" />
  <InteractiveCardMediaOverlay>
    <InteractiveCardMediaScrim />
    <InteractiveCardMediaCaption>North shore</InteractiveCardMediaCaption>
    <InteractiveCardMediaCorner>
      <IconButton icon={BookmarkSimple} label="Save this note" />
    </InteractiveCardMediaCorner>
  </InteractiveCardMediaOverlay>
</InteractiveCardMedia>

inset pulls the picture in by 8px and rounds it with rounded-lg, the card's own radius. Without it the picture runs to the card edge, and the card's clip rounds only the corners that touch that edge.

alt="" when the title names the subject. Give the image its own alt when the picture carries something the text does not.

On a vertical card the frame is the top of the card. A horizontal card keeps a 9rem media beside the text, and uses this same top frame once the card is narrower than 20rem. inset follows: beside the text, the picture is in from the outer edges, and across the top it is in from the sides and the top.

Patterns

pattern picks the one motion, on --duration-base (200ms) and --ease-out. lift is the default. The card rises 4px and its shadow steps from shadow-sm to shadow-md on hover and focus, then sets back down while it is pressed. zoom leaves the card still and scales the media to 1.05, then back to 1 while pressed. reveal keeps the actions out of view until hover or focus, then fades them up 4px. Until then they do not take the pointer, so a click follows the title. none is the same card with no travel.

A card with aria-disabled, or a button card with disabled, rests at shadow-sm, drops to 64% opacity, and does not open.

Hover a card, or tab to it. Press it and the lift or the zoom settles. On Reveal, Save is its own control: the title opens the note, and Save toggles without following the link.

The whole card or the title

asChild renders the card as its one child, so the whole card is one link or one button. That child is the only tab stop, and its name is all of the text inside it. Do not put another link or button inside it.

When the card needs a second control, leave asChild off and put InteractiveCardLink around the title. The link stretches over the card with an ::after, and InteractiveCardActions sits above that stretch, so the action receives the click once it is shown. The link's name is the title. asChild on InteractiveCardLink renders a router link the same way.

Use one of the two. A stretched title link inside a card that is already a link nests two interactive elements.

Props

InteractiveCard takes every <div> attribute, plus these.

PropTypeDefaultWhat it does
orientation"vertical" | "horizontal""vertical"vertical stacks the media on the text. horizontal places the media at the start of the row.
pattern"lift" | "zoom" | "reveal" | "none""lift"The motion on hover and focus. lift raises the card, zoom scales the media, reveal fades the actions in, and none stays still.
asChildbooleanfalseRenders the one child with the card's classes, so the card is a link or a button.

InteractiveCardTitle takes every heading attribute plus as, from "h1" to "h6", and it is an <h3> by default. InteractiveCardMedia takes every <div> attribute plus aspect, "wide", "square", or "tall", and "wide" is the default. inset pulls that frame in by 8px and rounds it with rounded-lg. InteractiveCardMediaOverlay takes every <div> attribute, and its slots are InteractiveCardMediaScrim, InteractiveCardMediaCaption (every <p> attribute), and InteractiveCardMediaCorner (every <div> attribute). InteractiveCardLink takes every <a> attribute plus asChild. The content, eyebrow, description, footer, meta, and actions take the attributes of their element. interactiveCardVariants({ orientation, pattern }) returns the classes, for styling something that is not an InteractiveCard.

Accessibility

  • With asChild, the child keeps its role. An <a> is a link and a <button> is a button, and Enter and Space work as they do natively. The accessible name is all of the text in the card, so write a title that stands on its own and do not add a second link or button inside. A picture with alt="" is skipped, so the title carries the name. The caption is read with the rest of the card, and on a link card it joins the link's name. A control in the corner is its own control, with its own name, so it does not belong inside a card that is already a link. The scrim is hidden.
  • InteractiveCardLink is one link whose name is its text, usually the title. The stretched hit area is the card. Actions are separate controls, later in the tab order, and they sit above the stretch so they receive the click once they are shown. Until hover or focus, a reveal card's actions do not take the pointer.
  • A plain card is a <div> with no role and no focus. The patterns respond to hover and to focus inside it, and the focus ring is 2px, inset, so the clipped corners do not cut it off. InteractiveCardTitle is an <h3> unless as picks another level.
  • aria-disabled on the card, and disabled on a button card, drop the opacity to 64%, keep the resting shadow, and block the pointer. On a link or button card, activation is cancelled too. The actions of a reveal card stay visible when the card is disabled, so the control does not disappear.
  • Lift, zoom, and reveal run only under motion-safe. Reduced motion leaves the card still, and a reveal card shows its actions at rest. The shadow step on lift still changes, without a transition.
  • The edge is a border and a shadow together. Forced colors drops shadows and keeps the border. The caption's scrim drops too, and the caption keeps a solid Canvas background so the line stays readable.
  • No color carries meaning. The contrast of text-fg-muted on the raised paper is not checked here.

When to use something else

  • Card for a static section of the page, a metric, or a short form. It does not move, and it is not the thing you open.
  • The email card for this frame in a letter. It is a picture beside an eyebrow, a title, a description, a meta line, and a button. It does not move, and the picture stacks over the copy on a phone.
  • Gallery to arrange several cards as rows, an even grid, or a masonry. The card is one item, and the gallery does not restyle it.
  • Item for one line of a list, with a small media, a title, and actions. An interactive card is a framed object, and an item is a row.
  • Radio group for a choice in a bordered card, where exactly one option is selected. RadioGroupCard is a radio, and an interactive card navigates or holds an action.
  • Hover card for a preview that floats beside a link. An interactive card is the link's surface on the page.

Install

shadcn add @gesso/interactive-card

On this page