Skip to content
Velocity X Docs
← All components

Bento Card Shells

Free · MIT

Single card shell component with five variants (icon, stat, image, chart-slot, quote) sharing one accessible article shell.

Category
Bento & Layout
Framework
astro
Version
1.0.0
Peer deps
none

Install

bash
velocityx add bento-card-shells

Free and MIT licensed — no account needed. The file lands under src/components/velocity/bento-card-shells/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add bento-card-shells writes into your project right now.

components/velocity/bento-card-shells/BentoCard.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * BentoCard — Shared card shell for bento grids, five variants:
 *
 *   icon   — icon chip + title + short description
 *   stat   — big number + label (+ optional trend string)
 *   image  — full-bleed media slot (named "media") with a caption strip
 *   chart  — reserved slot (named "chart") for any chart lib, with a
 *            title/subtitle header — this component renders NO chart,
 *            it only shells the slot so it stays dependency-free
 *   quote  — pull-quote styling with cite line
 *
 * Renders as <article> so a grid of cards reads as a list of landmarks.
 * Heading level is a prop — never hardcode h1/h2/h3 inside a reusable card.
 *
 * Usage:
 *   <BentoCard variant="stat" value="12.4k" label="Active users" trend="+18% MoM" />
 *   <BentoCard variant="icon" title="Encrypted at rest" description="AES-256, every row." headingLevel={3}>
 *     <svg slot="icon">...</svg>
 *   </BentoCard>
 *   <BentoCard variant="chart" title="Weekly signups" headingLevel={3}>
 *     <MyChart slot="chart" client:visible />
 *   </BentoCard>
 */
export interface Props {
  variant?: 'icon' | 'stat' | 'image' | 'chart' | 'quote';
  headingLevel?: 2 | 3 | 4 | 5 | 6;
  title?: string;
  subtitle?: string;
  description?: string;
  value?: string;
  label?: string;
  trend?: string;
  cite?: string;
  href?: string;
  className?: string;
}

const {
  variant = 'icon',
  headingLevel = 3,
  title,
  subtitle,
  description,
  value,
  label,
  trend,
  cite,
  href,
  className = '',
} = Astro.props;

const Heading = `h${headingLevel}`;
const Wrapper = href ? 'a' : 'div';
---

<article
  class:list={[
    'group relative flex h-full flex-col overflow-hidden rounded-2xl border',
    'border-secondary-200/70 dark:border-secondary-800/60',
    'bg-white dark:bg-secondary-900',
    href && 'transition-colors hover:border-primary-400/50 dark:hover:border-primary-600/40',
    className,
  ]}
>
  <Wrapper
    href={href}
    class:list={['flex h-full flex-col', href && 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary-500 focus-visible:outline-offset-2']}
  >
    {variant === 'icon' && (
      <div class="flex h-full flex-col gap-3 p-5 sm:p-6">
        {Astro.slots.has('icon') && (
          <span
            aria-hidden="true"
            class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary-500/10 text-primary-600 dark:bg-primary-400/15 dark:text-primary-300"
          >
            <slot name="icon" />
          </span>
        )}
        {title && (
          <Heading class="v-card-title font-semibold text-secondary-900 dark:text-white">
            {title}
          </Heading>
        )}
        {description && <p class="v-card-body text-secondary-600 dark:text-secondary-300">{description}</p>}
        <slot />
      </div>
    )}

    {variant === 'stat' && (
      <div class="flex h-full flex-col justify-between gap-3 p-5 sm:p-6">
        <div>
          {label && (
            <p class="h-eyebrow text-secondary-500 dark:text-secondary-400">{label}</p>
          )}
          {value && (
            <p class="mt-1 text-3xl sm:text-4xl font-semibold tracking-tight text-secondary-900 dark:text-white tabular-nums">
              {value}
            </p>
          )}
        </div>
        {trend && (
          <p class="text-sm font-medium text-primary-600 dark:text-primary-300">{trend}</p>
        )}
        <slot />
      </div>
    )}

    {variant === 'image' && (
      <>
        <div class="relative aspect-[4/3] w-full shrink-0 overflow-hidden bg-secondary-100 dark:bg-secondary-800">
          <slot name="media" />
        </div>
        {(title || description) && (
          <div class="flex flex-col gap-1 p-5 sm:p-6">
            {title && (
              <Heading class="v-card-title font-semibold text-secondary-900 dark:text-white">
                {title}
              </Heading>
            )}
            {description && <p class="v-card-body text-secondary-600 dark:text-secondary-300">{description}</p>}
          </div>
        )}
      </>
    )}

    {variant === 'chart' && (
      <div class="flex h-full flex-col gap-3 p-5 sm:p-6">
        {(title || subtitle) && (
          <div>
            {title && (
              <Heading class="v-card-title font-semibold text-secondary-900 dark:text-white">
                {title}
              </Heading>
            )}
            {subtitle && <p class="body-sm mt-0.5 text-secondary-500 dark:text-secondary-400">{subtitle}</p>}
          </div>
        )}
        <div class="min-h-[160px] flex-1">
          <slot name="chart" />
        </div>
      </div>
    )}

    {variant === 'quote' && (
      <figure class="flex h-full flex-col justify-between gap-4 p-5 sm:p-6">
        <blockquote class="v-card-body font-medium text-secondary-800 dark:text-secondary-100">
          <span aria-hidden="true" class="mr-1 text-primary-500">&ldquo;</span><slot />&rdquo;
        </blockquote>
        {cite && <figcaption class="body-sm text-secondary-500 dark:text-secondary-400">{cite}</figcaption>}
      </figure>
    )}
  </Wrapper>
</article>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.