Skip to content

Card

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 card

Sizes

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';