Card
ui
↳ lib-utils
Preview
Create project
Deploy your new project in one-click.
<div class="${cardClass()} w-full max-w-sm">
<div class=${cardHeaderClass()}>
<div class=${cardTitleClass()}>Create project</div>
<div class=${cardDescriptionClass()}>Deploy your new project in one-click.</div>
</div>
<div class=${cardContentClass()}>
<div class=${fieldClass()}>
<label class=${labelClass()} for="card-name">Name</label>
<input class=${inputClass()} id="card-name" placeholder="Name of your project">
</div>
</div>
<div class="${cardFooterClass()} justify-end gap-2">
<button class=${buttonClass({ variant: 'outline' })}>Cancel</button>
<button class=${buttonClass()}>Deploy</button>
</div>
</div>Installation
webjs ui add cardSizes
Default
Notifications
You have 3 unread.
Default size, gap-6 / py-6 / px-6.
<div class="${cardClass({ size: 'default' })} w-72" data-slot="card" data-size="default">
<div class=${cardHeaderClass()}>
<div class=${cardTitleClass()}>Notifications</div>
<div class=${cardDescriptionClass()}>You have 3 unread.</div>
</div>
<div class=${cardContentClass()}>Default size, gap-6 / py-6 / px-6.</div>
</div>Sm
Notifications
You have 3 unread.
Small size, gap-3 / py-3 / px-4.
<div class="${cardClass({ size: 'sm' })} w-72" data-slot="card" data-size="sm">
<div class=${cardHeaderClass()}>
<div class=${cardTitleClass()}>Notifications</div>
<div class=${cardDescriptionClass()}>You have 3 unread.</div>
</div>
<div class=${cardContentClass()}>Small size, gap-3 / py-3 / px-4.</div>
</div>API Reference
Parts
| Name | Description |
|---|---|
cardClass({ size }) |
Container. size="sm" yields gap-3/py-3; default is gap-6/py-6. |
cardHeaderClass() |
Header row. Picks up tighter padding when parent card has data-size="sm". |
cardTitleClass() / cardDescriptionClass() |
Prose inside the header. Title shrinks to text-sm under data-size="sm". |
cardActionClass() |
Right-aligned action area inside the header. |
cardContentClass() |
Body padding. Tighter under data-size="sm". |
cardFooterClass() |
Footer row. Tighter under data-size="sm". |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size |
"default" | "sm" |
"default" | Pass to cardClass AND set data-size on the same host so child helpers (header / title / content / footer) pick up the compact layout via group-data-[size=…]/card. |
Source: components/ui/card.ts
/**
* Card: visual container. Tier-1 class helpers; compose with `<div>`
* (or any element) for each subpart.
*
* shadcn parity:
* Card (size: default | sm) → cardClass({ size })
* CardHeader → cardHeaderClass()
* CardTitle → cardTitleClass()
* CardDescription → cardDescriptionClass()
* CardAction → cardActionClass()
* CardContent → cardContentClass()
* CardFooter → cardFooterClass()
*
* Design tokens used: --card, --card-foreground, --muted-foreground, --border.
*
* @example
* ```html
* <div class=${cardClass()}>
* <div class=${cardHeaderClass()}>
* <div class=${cardTitleClass()}>Notifications</div>
* <div class=${cardDescriptionClass()}>You have 3 unread messages.</div>
* <div class=${cardActionClass()}>
* <button class=${buttonClass({ variant: 'ghost', size: 'sm' })}>Mark all read</button>
* </div>
* </div>
* <div class=${cardContentClass()}>
* <p class="text-sm">Your weekly digest is ready to review.</p>
* </div>
* <div class=${cardFooterClass()}>
* <button class=${buttonClass()}>Save</button>
* </div>
* </div>
* ```
*/
export type CardSize = 'default' | 'sm';
/**
* Card root. shadcn ships `size?: "default" | "sm"` on Card across
* 14/15 style families (only new-york-v4 omits it). The class string
* uses `group/card` so the header / title / content / footer helpers
* can read the parent card's data-size and adjust their own padding
* + gap.
*
* USAGE: pass size to cardClass AND set data-size="<size>" on the
* same host element so the group-data-[size=...]/card child rules
* fire. Set `data-slot="card"` for shadcn parity.
*
* <div class=${cardClass({ size: 'sm' })} data-slot="card" data-size="sm">
* <div class=${cardHeaderClass()}>...</div>
* ...
* </div>
*
* Sizes:
* default: gap-6 / py-6 (shadcn new-york-v4 default)
* sm: gap-3 / py-3 (shadcn radix-nova + base-* defaults)
*/
export const cardClass = (opts: { size?: CardSize } = {}): string => {
const size = opts.size ?? 'default';
const base =
'group/card flex flex-col rounded-xl border bg-card text-card-foreground shadow-sm';
return size === 'sm' ? base + ' gap-3 py-3' : base + ' gap-6 py-6';
};
/**
* Card header: supports an optional `CardAction` slot via grid layout.
* group-data-[size=sm]/card rules pick up the compact layout when the
* root card carries data-size="sm".
*/
export const cardHeaderClass = (): string =>
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:gap-1 group-data-[size=sm]/card:[.border-b]:pb-3';
/** Card title: heading text within the header. Smaller when card is data-size="sm". */
export const cardTitleClass = (): string =>
'leading-none font-semibold group-data-[size=sm]/card:text-sm';
/** Card description: subdued caption beneath the title. */
export const cardDescriptionClass = (): string => 'text-sm text-muted-foreground';
/** Card action: right-aligned controls inside the header (matches shadcn CardAction). */
export const cardActionClass = (): string =>
'col-start-2 row-span-2 row-start-1 self-start justify-self-end';
/** Card content: the main body region. Tighter padding when card is data-size="sm". */
export const cardContentClass = (): string =>
'px-6 group-data-[size=sm]/card:px-4';
/** Card footer: trailing controls or actions. Tighter padding when card is data-size="sm". */
export const cardFooterClass = (): string =>
'flex items-center px-6 [.border-t]:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:[.border-t]:pt-3';