Bento Card Shells
Free · MITSingle 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
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
---
// 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">“</span><slot />”
</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.