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 totransparentvariant).card--default- Standard appearance with surface-secondary (default).card--secondary- Medium prominence with surface-tertiary (maps tosecondaryvariant).card--tertiary- Higher prominence with surface-tertiary (maps totertiaryvariant)
API Reference
Card
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "transparent" | "default" | "secondary" | "tertiary" | "default" | Semantic variant indicating prominence level |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Card content |
Card.Header
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Header content |
Card.Title
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Title content (renders as h3) |
Card.Description
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Description content (renders as p) |
Card.Content
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Main content |
Card.Footer
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.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>