Skip to content
Lenso UI

Card

Flexible container component for grouping related content and actions

Usage

import { Card } from "@lenso/ui";
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.// oxlint-disable jsx-a11y/prefer-tag-over-role -- A named SVG needs img semantics; an HTML img cannot render this icon.import { CircleDollar } from "@gravity-ui/icons";import { Card, Link } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
export function Default() {  return (    <Card xstyle={s.card400}>      <CircleDollar        aria-label="Dollar sign icon"        role="img"        {...stylex.props(s.primary, s.icon6)}      />      <Card.Header>        <Card.Title>Become an Acme Creator!</Card.Title>        <Card.Description>          Visit the Acme Creator Hub to sign up today and start earning credits from your fans and          followers.        </Card.Description>      </Card.Header>      <Card.Footer>        <Link          aria-label="Go to Acme Creator Hub (opens in new tab)"          href="https://heroui.com"          rel="noopener noreferrer"          target="_blank"        >          Creator Hub <Link.Icon aria-hidden="true" />        </Link>      </Card.Footer>    </Card>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import { Card } from "@lenso/ui";
export default () => (  <Card>    <Card.Header>      <Card.Title />      <Card.Description />    </Card.Header>    <Card.Content />    <Card.Footer />  </Card>);

Examples

Variants

Cards come in semantic variants that describe their prominence level rather than specific visual styles. This allows themes to interpret them differently:

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Card } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
const cards = [  {    variant: "transparent",    title: "Transparent",    description: "Minimal prominence with transparent background",    content: "Use for less important content or nested cards",  },  {    variant: "default",    title: "Default",    description: "Standard card appearance (bg-surface)",    content: "The default card variant for most use cases",  },  {    variant: "secondary",    title: "Secondary",    description: "Medium prominence (bg-surface-secondary)",    content: "Use to draw moderate attention",  },  {    variant: "tertiary",    title: "Tertiary",    description: "Higher prominence (bg-surface-tertiary)",    content: "Use for primary or featured content",  },] as const;export function Variants() {  return (    <div {...stylex.props(s.column4)}>      {cards.map((card) => (        <Card key={card.variant} xstyle={s.card320} variant={card.variant}>          <Card.Header>            <Card.Title>{card.title}</Card.Title>            <Card.Description>{card.description}</Card.Description>          </Card.Header>          <Card.Content>            <p>{card.content}</p>          </Card.Content>        </Card>      ))}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

  • transparent - Minimal prominence, transparent background (great for nested cards)
  • default - Standard card for most use cases (surface-secondary)
  • secondary - Medium prominence to draw moderate attention (surface-tertiary)
  • tertiary - Higher prominence for important content (surface-tertiary)

Horizontal Layout

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Button, Card, CloseButton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
export function Horizontal() {  return (    <Card xstyle={s.horizontal}>      <div {...stylex.props(s.imageFrame)}>        <img          alt="Cherries"          {...stylex.props(s.imageZoom)}          loading="lazy"          src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/cherries.jpeg"        />      </div>      <div {...stylex.props(s.flexContent)}>        <Card.Header xstyle={s.gap1}>          <Card.Title xstyle={s.titleInset}>Become an ACME Creator!</Card.Title>          <Card.Description>            Lorem ipsum dolor sit amet consectetur. Sed arcu donec id aliquam dolor sed amet            faucibus etiam.          </Card.Description>          <CloseButton aria-label="Close banner" xstyle={s.close} />        </Card.Header>        <Card.Footer xstyle={s.footer}>          <div {...stylex.props(s.column)}>            <span {...stylex.props(s.textSm, s.medium, s.foreground)}>Only 10 spots</span>            <span {...stylex.props(s.textXs, s.muted)}>Submission ends Oct 10.</span>          </div>          <Button xstyle={s.fullAuto}>Apply Now</Button>        </Card.Footer>      </div>    </Card>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Avatar

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, Card } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
export function WithAvatar() {  return (    <div {...stylex.props(s.wrap4)}>      <Card xstyle={s.card200}>        <img          alt="Indie Hackers community"          {...stylex.props(s.imageCommunity)}          loading="lazy"          src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/demo1.jpg"        />        <Card.Header>          <Card.Title>Indie Hackers</Card.Title>          <Card.Description>148 members</Card.Description>        </Card.Header>        <Card.Footer xstyle={s.row2}>          <Avatar aria-label="Martha's profile picture" xstyle={s.avatarTiny}>            <Avatar.Image              alt="Martha's avatar"              src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg"            />            <Avatar.Fallback xstyle={s.textXs}>IH</Avatar.Fallback>          </Avatar>          <span {...stylex.props(s.textXs)}>By Martha</span>        </Card.Footer>      </Card>      <Card xstyle={s.card200}>        <img          alt="AI Builders community"          {...stylex.props(s.imageCommunity)}          loading="lazy"          src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/demo2.jpg"        />        <Card.Header>          <Card.Title>AI Builders</Card.Title>          <Card.Description>362 members</Card.Description>        </Card.Header>        <Card.Footer xstyle={s.row2}>          <Avatar aria-label="John's profile picture" xstyle={s.avatarTiny}>            <Avatar.Image              alt="John's avatar - blue themed"              src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"            />            <Avatar.Fallback xstyle={s.textXs}>B</Avatar.Fallback>          </Avatar>          <span {...stylex.props(s.textXs)}>By John</span>        </Card.Footer>      </Card>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Images

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.// oxlint-disable jsx-a11y/prefer-tag-over-role -- A named SVG needs img semantics; an HTML img cannot render this icon.import { CircleDollar } from "@gravity-ui/icons";import { Avatar, Button, Card, CloseButton, Link } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
const DOCS = "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/";const AVATARS = "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/";
export function WithImages() {  return (    <div {...stylex.props(s.gridOuter)}>      <div {...stylex.props(s.grid)}>        <Card xstyle={s.firstCard}>          <div {...stylex.props(s.imageFrame)}>            <img              alt="Cherries"              {...stylex.props(s.imageZoom)}              loading="lazy"              src={`${DOCS}cherries.jpeg`}            />          </div>          <div {...stylex.props(s.flexContent)}>            <Card.Header xstyle={s.gap1}>              <Card.Title xstyle={s.titleInset}>Become an ACME Creator!</Card.Title>              <Card.Description>                Lorem ipsum dolor sit amet consectetur. Sed arcu donec id aliquam dolor sed amet                faucibus etiam.              </Card.Description>              <CloseButton aria-label="Close banner" xstyle={s.close} />            </Card.Header>            <Card.Footer xstyle={s.footer}>              <div {...stylex.props(s.column)}>                <span {...stylex.props(s.textSm, s.medium, s.foreground)}>Only 10 spots</span>                <span {...stylex.props(s.textXs, s.muted)}>Submission ends Oct 10.</span>              </div>              <Button xstyle={s.fullAuto}>Apply Now</Button>            </Card.Footer>          </div>        </Card>        <div {...stylex.props(s.gridRow)}>          <div {...stylex.props(s.gridHalf)}>            <Card xstyle={s.span12}>              <div {...stylex.props(s.close, s.z10)}>                <CloseButton aria-label="Close notification" />              </div>              <Card.Header xstyle={s.gap3}>                <CircleDollar                  aria-label="Dollar sign icon"                  {...stylex.props(s.primary, s.icon8, s.shrink0)}                  role="img"                />                <div {...stylex.props(s.column1)}>                  <span {...stylex.props(s.textXs, s.medium, s.muted, s.uppercase)}>PAYMENT</span>                  <Card.Title xstyle={s.responsiveTitle}>You can now withdraw on crypto</Card.Title>                  <Card.Description xstyle={s.responsiveXs}>                    Add your wallet in settings to withdraw                  </Card.Description>                </div>              </Card.Header>              <Card.Footer>                <Link aria-label="Go to settings" href="#" rel="noopener noreferrer">                  Go to settings                  <Link.Icon aria-hidden="true" />                </Link>              </Card.Footer>            </Card>            <div {...stylex.props(s.gridRow)}>              <Card xstyle={s.smallCard}>                <Card.Header>                  <Avatar xstyle={s.roundedAvatar}>                    <Avatar.Image alt="Demo 1" src={`${DOCS}demo1.jpg`} />                    <Avatar.Fallback>JK</Avatar.Fallback>                  </Avatar>                </Card.Header>                <Card.Content xstyle={s.mt1}>                  <p {...stylex.props(s.textSm, s.leading4, s.medium)}>Indie Hackers</p>                  <p {...stylex.props(s.textXs, s.muted)}>148 members</p>                </Card.Content>                <Card.Footer xstyle={s.row2}>                  <Avatar xstyle={s.avatarMini}>                    <Avatar.Image alt="John" src={`${AVATARS}red.jpg`} />                    <Avatar.Fallback>JK</Avatar.Fallback>                  </Avatar>                  <p {...stylex.props(s.textXs, s.muted)}>By John</p>                </Card.Footer>              </Card>              <Card xstyle={s.smallCard}>                <Card.Header>                  <Avatar xstyle={s.roundedAvatar}>                    <Avatar.Image alt="Demo 2" src={`${DOCS}demo2.jpg`} />                    <Avatar.Fallback>AB</Avatar.Fallback>                  </Avatar>                </Card.Header>                <Card.Content xstyle={s.mt1}>                  <p {...stylex.props(s.textSm, s.leading4, s.medium)}>AI Builders</p>                  <p {...stylex.props(s.textXs, s.muted)}>362 members</p>                </Card.Content>                <Card.Footer xstyle={s.row2}>                  <Avatar xstyle={s.avatarMini}>                    <Avatar.Image alt="John" src={`${AVATARS}blue.jpg`} />                    <Avatar.Fallback>M</Avatar.Fallback>                  </Avatar>                  <p {...stylex.props(s.textXs, s.muted)}>By Martha</p>                </Card.Footer>              </Card>            </div>          </div>          <Card xstyle={s.robotCard}>            <img              alt="NEO Home Robot"              aria-hidden="true"              {...stylex.props(s.imageCover)}              src={`${DOCS}neo2.jpeg`}            />            <Card.Header xstyle={[s.z10, s.white]}>              <Card.Title xstyle={[s.textXs, s.semibold, s.tracking, s.black70]}>NEO</Card.Title>              <Card.Description xstyle={[s.textSm, s.medium, s.black50]}>                Home Robot              </Card.Description>            </Card.Header>            <Card.Footer xstyle={s.robotFooter}>              <div>                <div {...stylex.props(s.textSm, s.medium, s.black)}>Available soon</div>                <div {...stylex.props(s.textXs, s.black60)}>Get notified</div>              </div>              <Button xstyle={s.whiteButton} size="sm" variant="tertiary">                Notify me              </Button>            </Card.Footer>          </Card>        </div>        <div {...stylex.props(s.gridRow)}>          <Card xstyle={s.robotLarge}>            <img              alt="NEO Home Robot"              aria-hidden="true"              {...stylex.props(s.imageCover)}              src={`${DOCS}neo1.jpeg`}            />            <Card.Footer xstyle={s.robotLargeFooter}>              <div>                <div {...stylex.props(s.responsiveBase, s.medium, s.black)}>NEO</div>                <div {...stylex.props(s.responsiveXs, s.medium, s.black50)}>$499/m</div>              </div>              <Button xstyle={s.whiteButton} size="sm" variant="tertiary">                Get now              </Button>            </Card.Footer>          </Card>          <div {...stylex.props(s.robotStack)}>            <Card xstyle={s.eventCard} variant="transparent">              <img                alt="Futuristic Robot"                {...stylex.props(s.eventImage)}                loading="lazy"                src={`${DOCS}robot1.jpeg`}              />              <div {...stylex.props(s.eventContent)}>                <Card.Title xstyle={s.textSm}>Bridging the Future</Card.Title>                <Card.Description xstyle={s.textXs}>Today, 6:30 PM</Card.Description>              </div>            </Card>            <Card xstyle={s.eventCard} variant="transparent">              <img                alt="Avocado"                {...stylex.props(s.eventImage)}                loading="lazy"                src={`${DOCS}avocado.jpeg`}              />              <div {...stylex.props(s.eventContent)}>                <Card.Title xstyle={s.textSm}>Avocado Hackathon</Card.Title>                <Card.Description xstyle={s.textXs}>Wed, 4:30 PM</Card.Description>              </div>            </Card>            <Card xstyle={s.eventCard} variant="transparent">              <img                alt="Sound Electro event"                {...stylex.props(s.eventImage)}                loading="lazy"                src={`${DOCS}oranges.jpeg`}              />              <div {...stylex.props(s.eventContent)}>                <Card.Title xstyle={s.textSm}>Sound Electro | Beyond art</Card.Title>                <Card.Description xstyle={s.textXs}>Fri, 8:00 PM</Card.Description>              </div>            </Card>          </div>        </div>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Form

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Button, Card, Form, Input, Label, Link, TextField } from "@lenso/ui";import type { FormEvent } from "react";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
export function WithForm() {  const onSubmit = (event: FormEvent<HTMLFormElement>) => {    event.preventDefault();    window.alert("Form submitted successfully!");  };  return (    <Card xstyle={s.maxMd}>      <Card.Header>        <Card.Title>Login</Card.Title>        <Card.Description>Enter your credentials to access your account</Card.Description>      </Card.Header>      <Form onSubmit={onSubmit}>        <Card.Content>          <div {...stylex.props(s.column4)}>            <TextField name="email">              <Label>Email</Label>              <Input                name="email"                type="email"                placeholder="[email protected]"                variant="secondary"              />            </TextField>            <TextField name="password">              <Label>Password</Label>              <Input name="password" type="password" placeholder="••••••••" variant="secondary" />            </TextField>          </div>        </Card.Content>        <Card.Footer xstyle={s.formFooter}>          <Button xstyle={s.full} type="submit">            Sign In          </Button>          <Link xstyle={[s.centerText, s.textSm]} href="#">            Forgot password?          </Link>        </Card.Footer>      </Form>    </Card>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Check, Star } from "@gravity-ui/icons";import { Button, Card } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "./display.stylex";
const PRO_FEATURES = [  "Unlimited projects and collaborators",  "Priority support with 24h response",  "Advanced analytics and exports",] as const;const styles = stylex.create({  card: {    position: "relative",    width: "100%",    maxWidth: 448,    overflow: "hidden",    borderWidth: 1,    borderStyle: "solid",    borderColor: {      default: "color-mix(in oklab, var(--accent) 20%, transparent)",      ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--accent) 30%, transparent)",    },    backgroundImage: {      default:        "linear-gradient(to bottom right in oklab, color-mix(in oklab, var(--accent) 12%, transparent), var(--surface), var(--surface-secondary))",      ':is([data-theme="dark"] *)':        "linear-gradient(to bottom right in oklab, color-mix(in oklab, var(--accent) 20%, transparent), var(--surface), color-mix(in oklab, var(--accent) 8%, transparent))",    },    boxShadow: {      default:        "0 10px 15px -3px color-mix(in oklab, var(--accent) 10%, transparent), 0 4px 6px -4px color-mix(in oklab, var(--accent) 10%, transparent)",      ':is([data-theme="dark"] *)':        "0 10px 15px -3px color-mix(in oklab, var(--accent) 5%, transparent), 0 4px 6px -4px color-mix(in oklab, var(--accent) 5%, transparent)",    },  },  topGlow: {    pointerEvents: "none",    position: "absolute",    top: -48,    right: -48,    width: 160,    height: 160,    borderRadius: 9999,    filter: "blur(64px)",    backgroundColor: {      default: "color-mix(in oklab, var(--accent) 20%, transparent)",      ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--accent) 30%, transparent)",    },  },  bottomGlow: {    pointerEvents: "none",    position: "absolute",    bottom: -32,    left: -32,    width: 112,    height: 112,    borderRadius: 9999,    filter: "blur(40px)",    backgroundColor: {      default: "color-mix(in oklab, var(--accent) 10%, transparent)",      ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--accent) 20%, transparent)",    },  },  recommended: {    width: "fit-content",    borderRadius: 9999,    paddingInline: 10,    paddingBlock: 2,    fontSize: 12,    lineHeight: "16px",    fontWeight: 500,    letterSpacing: "0.025em",    backgroundColor: {      default: "color-mix(in oklab, var(--accent) 15%, transparent)",      ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--accent) 25%, transparent)",    },    color: {      default: "var(--accent)",      ':is([data-theme="dark"] *)': "var(--accent-soft-foreground)",    },  },  star: {    display: "flex",    width: 40,    height: 40,    flexShrink: 0,    alignItems: "center",    justifyContent: "center",    borderRadius: 12,    backgroundColor: {      default: "color-mix(in oklab, var(--accent) 15%, transparent)",      ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--accent) 20%, transparent)",    },    color: {      default: "var(--accent)",      ':is([data-theme="dark"] *)': "var(--accent-soft-foreground)",    },  },  footer: {    position: "relative",    flexDirection: { default: "column", "@media (min-width: 640px)": "row" },    gap: 8,  },  upgrade: {    width: "100%",    boxShadow:      "0 4px 6px -1px color-mix(in oklab, var(--accent) 20%, transparent), 0 2px 4px -2px color-mix(in oklab, var(--accent) 20%, transparent)",  },});export function CustomStyles() {  return (    <Card xstyle={styles.card}>      <div aria-hidden="true" {...stylex.props(styles.topGlow)} />      <div aria-hidden="true" {...stylex.props(styles.bottomGlow)} />      <Card.Header xstyle={[s.relative, s.gap3]}>        <span {...stylex.props(styles.recommended)}>Recommended</span>        <div {...stylex.props(s.startRow3)}>          <div {...stylex.props(styles.star)}>            <Star aria-hidden="true" {...stylex.props(s.icon5)} />          </div>          <div {...stylex.props(s.column1)}>            <Card.Title>Upgrade to Pro</Card.Title>            <Card.Description>              Unlock team workflows and insights built for growing products.            </Card.Description>          </div>        </div>      </Card.Header>      <Card.Content xstyle={s.relative}>        <ul {...stylex.props(s.column2)}>          {PRO_FEATURES.map((feature) => (            <li key={feature} {...stylex.props(s.row2, s.textSm, s.muted)}>              <Check aria-hidden="true" {...stylex.props(s.icon4, s.shrink0, s.accent)} />              {feature}            </li>          ))}        </ul>      </Card.Content>      <Card.Footer xstyle={styles.footer}>        <Button xstyle={styles.upgrade}>Upgrade now</Button>        <Button xstyle={s.full} variant="secondary">          Compare plans        </Button>      </Card.Footer>    </Card>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

To customize the Card component classes, you can use the @layer components directive. Learn more.

@layer components {  .card--secondary {    @apply bg-gradient-to-br from-blue-50 to-purple-50;  }
  .card__title {    @apply text-xl font-bold;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

Card uses BEM naming for predictable styling (View source styles):

Base Classes [!toc]

  • .card - Base container with padding and border
  • .card__header - Header section container
  • .card__title - Title with base font size and weight
  • .card__description - Muted description text
  • .card__content - Flexible content container
  • .card__footer - Footer with row layout

Variant Classes [!toc]

  • .card--transparent - Minimal prominence, transparent background (maps to transparent variant)
  • .card--default - Standard appearance with surface-secondary (default)
  • .card--secondary - Medium prominence with surface-tertiary (maps to secondary variant)
  • .card--tertiary - Higher prominence with surface-tertiary (maps to tertiary variant)

API Reference

Card

PropTypeDefaultDescription
variant"transparent" | "default" | "secondary" | "tertiary""default"Semantic variant indicating prominence level
classNamestring-Additional CSS classes
childrenReact.ReactNode-Card content

Card.Header

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Header content

Card.Title

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Title content (renders as h3)

Card.Description

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Description content (renders as p)

Card.Content

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Main content

Card.Footer

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Footer content

Accessibility

import { Card } from '@lenso/ui';import { cardVariants } from '@lenso/tokens';
// Semantic markup<Card role="article" aria-labelledby="card-title">  <Card.Header>    <Card.Title id="card-title">Article Title</Card.Title>  </Card.Header></Card>
// Interactive cards<a className={cardVariants().base()} href="/details" aria-label="View product details">  <Card.Title>Product Name</Card.Title></a>