v1.0

Card

Preview

Basic

Loading…

Preview

Code

ts
import Card from '@/components/layout/Card';

src/components/layout/Card.tsx

AI prompt

text
Build a card (panel surface) component in React + TypeScript + Tailwind CSS.

The frosted panel every surface sits on, as a component with an optional header, actions and footer.

## Look
- `<section>`: the frosted card surface, `flex flex-col`.
- `solid`: the same card with no translucency or backdrop blur (opaque `bg-white dark:bg-slate-800`) — for anything that floats OVER page content, where a translucent surface lets the rows behind read through.
- Header (rendered when `title` or `actions` is set): `flex items-start justify-between gap-3 border-b border-slate-200 dark:border-slate-700 px-4 py-3`. Left, in a `min-w-0` block: title as `<h3>` `text-sm font-semibold text-slate-800 dark:text-slate-100`, subtitle `mt-0.5 text-[11px] text-slate-500 dark:text-slate-400`. Right: actions in `flex shrink-0 items-center gap-2`.
- Body: `min-h-0 flex-1`, with `p-4` unless `padded={false}` (for a full-bleed table or list).
- Footer: `border-t border-slate-200 dark:border-slate-700 px-4 py-2.5`.

## API
`title?`, `subtitle?`, `actions?`, `footer?`, `children?`: ReactNode; `solid?` (false); `padded?` (true); `className`.

## Demo
Two cards in `grid gap-3 sm:grid-cols-2`: "Usage" / "Last 30 days" with a small ghost "Export" button as its action; and "Plan", `solid`, with a small primary "Upgrade" button in the footer.

## House style (applies to everything above)
- Stack: React 19 + TypeScript + Tailwind CSS v4, icons from lucide-react. One self-contained file; default-export the component and named-export its types. `'use client'` if it has state, refs or handlers.
- Font Inter; palette indigo on slate. Primary accent indigo-600 (hover indigo-700, dark mode indigo-400). Body text slate-700 / dark slate-200; secondary slate-500 / dark slate-400.
- Dark mode is a `.dark` class on <html> (not prefers-color-scheme). Every colour needs its `dark:` pair.
- Compact admin scale: text-xs (12px) for controls and body, 10–11px for meta, rounded-lg (8px) controls, rounded-2xl (16px) cards.
- Card surface ("panel"): `bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-lg`, on a soft slate gradient page background.
- Floating surfaces (dropdowns, popovers, menus) are OPAQUE: `bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-lg`, no backdrop blur (it creates a stacking context that traps the popover's z-index). In-flow popovers are z-50; portalled overlays z-200.
- Text inputs and select triggers: `w-full px-3 py-2 text-xs rounded-lg border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/60 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500`.
- Field labels: 11px semibold slate-600. Section titles: 10px semibold uppercase wide-tracking slate-500.
- Primary button: indigo-600 fill, white 12px semibold text, rounded-lg, px-3 py-2, disabled at 50% opacity. Ghost button: slate-600 text, hover slate-100.
- Popovers close on outside click AND on Escape (listen to both; include the portalled panel's element in the outside-click check).
- Don't nest scroll containers around popovers: an ancestor with overflow hidden/auto clips an absolutely-positioned dropdown. Portal the panel to <body> when it must escape a scroller, and reposition it on scroll and resize.
- Accessible by default: visible focus rings, keyboard support that matches the WAI-ARIA pattern for the widget, `aria-label` on icon-only buttons, `min-w-0` so text truncates instead of overflowing.

Source

tsx
'use client';

import { cn } from '@/lib/cn';

/**
 * The frosted panel every surface in this library sits on, as a component.
 *
 * `solid` turns off the backdrop blur — needed for anything that floats OVER
 * page content, where a translucent surface lets the rows behind read through.
 * The CSS recipes (`.panel`, `.panel-solid`) stay available for markup that
 * cannot take a component.
 */
export default function Card({
  title,
  subtitle,
  actions,
  footer,
  solid = false,
  padded = true,
  className,
  children,
}: {
  title?: React.ReactNode;
  subtitle?: React.ReactNode;
  actions?: React.ReactNode;
  footer?: React.ReactNode;
  solid?: boolean;
  padded?: boolean;
  className?: string;
  children?: React.ReactNode;
}) {
  return (
    <section className={cn('panel', solid && 'panel-solid', 'flex flex-col', className)}>
      {(title || actions) && (
        <header className="flex items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-700">
          <div className="min-w-0">
            {title && <h3 className="text-sm font-semibold text-slate-800 dark:text-slate-100">{title}</h3>}
            {subtitle && <p className="mt-0.5 text-[11px] text-slate-500 dark:text-slate-400">{subtitle}</p>}
          </div>
          {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
        </header>
      )}
      <div className={cn('min-h-0 flex-1', padded && 'p-4')}>{children}</div>
      {footer && (
        <footer className="border-t border-slate-200 px-4 py-2.5 dark:border-slate-700">{footer}</footer>
      )}
    </section>
  );
}

Props

PropTypeDefaultDescription
titleReact.ReactNode—
subtitleReact.ReactNode—
actionsReact.ReactNode—
footerReact.ReactNode—
solidbooleanfalse
paddedbooleantrue
classNamestring—
childrenReact.ReactNode—