Bento Card
A feature-grid tile built on a raised panel, a recessed stage for the visual, and a scroll-in reveal.
Visual workflow builder
Drag triggers, conditions, and actions into place. Ship automations in minutes, not sprints.
Learn moreSupport that answers itself
AI agents resolve tickets around the clock.
Revenue in real time
Every metric current, every chart live.
View dashboardSecure by default
SSO, encryption, and audit trails out of the box.
- now
- 2m
- 5m
Never miss a lead
Notified the moment anything happens.
BentoGrid lays out BentoCard children and staggers their reveal automatically.
Each card is a raised top-lit panel with a recessed stage carved into it for the
visual; link cards lift on hover while static cards stay put, so motion always
signals something clickable. Built with Motion.
Installation
Complete the shared Setup first, then add Motion.
bun add motionCopy the component into components/ui/bento-card.tsx.
'use client';import * as React from 'react';import { motion, useReducedMotion } from 'motion/react';import { ArrowUpRight } from 'lucide-react';import { cn } from '@/lib/cn';const EASE_OUT = [0.23, 1, 0.32, 1] as const;const FOCUS = 'focus-visible:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--bento-accent)]';const EASE_CSS = '[transition-timing-function:cubic-bezier(0.23,1,0.32,1)]';export type BentoGridProps = { children: React.ReactNode; columns?: 2 | 3 | 4; className?: string;};export function BentoGrid({ children, columns = 3, className }: BentoGridProps) { const items = React.Children.toArray(children); return ( <div className={cn( 'grid grid-cols-1 gap-3 sm:grid-cols-2', columns === 3 && 'lg:grid-cols-3', columns === 4 && 'lg:grid-cols-4', className, )} > {items.map((child, i) => React.isValidElement<BentoCardProps>(child) && child.type === BentoCard ? React.cloneElement(child, { delay: child.props.delay ?? i * 0.06, }) : child, )} </div> );}export type BentoCardProps = { title: string; description?: string; icon?: React.ReactNode; badge?: string; span?: 1 | 2 | 3; href?: string; cta?: string; accent?: string; fade?: boolean; delay?: number; children?: React.ReactNode; className?: string;};export function BentoCard({ title, description, icon, badge, span = 1, href, cta = 'Learn more', accent = '#f0883e', fade = true, delay = 0, children, className,}: BentoCardProps) { const reduce = useReducedMotion(); const hidden = reduce ? { opacity: 0 } : { opacity: 0, y: 12, filter: 'blur(4px)' }; const shown = reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: 'blur(0px)' }; const Root = (href ? motion.a : motion.div) as typeof motion.a; return ( <Root href={href} style={{ '--bento-accent': accent } as React.CSSProperties} initial={hidden} whileInView={shown} viewport={{ once: true, margin: '-60px' }} transition={{ duration: 0.45, ease: EASE_OUT, delay }} className={cn( 'bloom-edge group relative flex flex-col overflow-hidden rounded-[14px] p-1.5', 'bg-gradient-to-b from-white to-[#f4f4f5] text-neutral-900', 'dark:from-[#1d1d1d] dark:to-[#161716] dark:text-white', 'shadow-[0_1px_2px_0_rgba(0,0,0,0.05),0_5px_14px_-10px_rgba(0,0,0,0.1)]', 'dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05),0_1px_2px_0_rgba(0,0,0,0.4),0_6px_16px_-10px_rgba(0,0,0,0.45)]', span === 2 && 'sm:col-span-2', span === 3 && 'sm:col-span-2 lg:col-span-3', href && [ 'cursor-pointer no-underline', `transition-[translate,scale,box-shadow] duration-200 ${EASE_CSS}`, 'hover:-translate-y-px', 'hover:shadow-[0_1px_2px_0_rgba(0,0,0,0.05),0_14px_30px_-12px_rgba(0,0,0,0.18)]', 'dark:hover:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.07),0_1px_2px_0_rgba(0,0,0,0.4),0_16px_36px_-12px_rgba(0,0,0,0.6)]', 'active:scale-[0.98] active:duration-100', 'motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:active:scale-100', FOCUS, ], className, )} > {badge && ( <span className="pointer-events-none absolute top-3 right-3 z-10 rounded-full px-2 py-0.5 text-[11px] font-medium" style={{ color: 'var(--bento-accent)', backgroundColor: 'color-mix(in srgb, var(--bento-accent) 14%, transparent)', }} > {badge} </span> )} {children && ( <div className={cn( 'bloom-edge relative flex-1 overflow-hidden rounded-[11px]', 'bg-black/[0.02] dark:bg-black/20', 'shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.04)] dark:shadow-[inset_0_1px_3px_0_rgba(0,0,0,0.5)]', )} > <div className={cn( 'relative flex h-full flex-col justify-center', fade && '[mask-image:linear-gradient(to_bottom,black_55%,transparent_98%)]', )} > {children} </div> </div> )} <div className="mt-auto flex flex-col px-3 pt-4 pb-3"> {icon && ( <span aria-hidden className={cn( 'bloom-edge mb-3 grid size-9 place-items-center rounded-[10px]', 'bg-gradient-to-b from-white to-[#f1f1f2] dark:from-[#272727] dark:to-[#1d1d1d]', 'shadow-[inset_0_1px_0_0_rgba(255,255,255,0.9),0_1px_2px_0_rgba(0,0,0,0.08)]', 'dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.08),0_1px_2px_0_rgba(0,0,0,0.5)]', 'text-neutral-700 dark:text-neutral-200', )} > {icon} </span> )} <h3 className="text-[14px] font-medium tracking-[-0.01em]">{title}</h3> {description && ( <p className="mt-1 text-[13px] leading-relaxed text-neutral-500 dark:text-[#929292]"> {description} </p> )} {href && ( <span className={cn( 'mt-3 inline-flex items-center gap-1 text-[12px] font-medium', 'text-neutral-500 transition-colors duration-200 dark:text-[#929292]', 'group-hover:text-neutral-900 dark:group-hover:text-white', )} > {cta} <ArrowUpRight size={13} strokeWidth={2} aria-hidden className={cn( `transition-[translate] duration-200 ${EASE_CSS}`, 'group-hover:translate-x-px group-hover:-translate-y-px', 'motion-reduce:transition-none motion-reduce:group-hover:translate-x-0 motion-reduce:group-hover:translate-y-0', )} /> </span> )} </div> </Root> );}Usage
import { Sparkles } from 'lucide-react';
import { BentoCard, BentoGrid } from '@/components/ui/bento-card';
export default function Example() {
return (
<BentoGrid columns={3}>
<BentoCard
span={2}
icon={<Sparkles size={18} strokeWidth={1.75} />}
title="Visual workflow builder"
description="Drag triggers, conditions, and actions into place."
badge="New"
href="#"
/>
<BentoCard
icon={<Sparkles size={18} strokeWidth={1.75} />}
title="Support that answers itself"
description="AI agents resolve tickets around the clock."
/>
</BentoGrid>
);
}Examples
Text-only tiles: the frame carries the section even with no visuals.
Props
BentoGrid
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | BentoCard elements to lay out. |
columns | 2 | 3 | 4 | 3 | Columns at the largest breakpoint. |
className | string | - | Forwarded to the grid. |
BentoCard
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | - | Card title. |
description | string | - | Card description. |
icon | React.ReactNode | - | A lucide icon rendered in a raised icon chip. |
badge | string | - | Accent tint pill in the top-right corner. |
span | 1 | 2 | 3 | 1 | Column span from the sm breakpoint up. |
href | string | - | Renders the card as a link; only links lift. |
cta | string | "Learn more" | Label for the link row shown when href is set. |
accent | string | "#f0883e" | Focus ring + badge tint. |
fade | boolean | true | Fade the bottom of the visual into the card. |
delay | number | - | Reveal delay in seconds; BentoGrid sets this. |
children | React.ReactNode | - | The visual, rendered in the recessed stage. |
className | string | - | Forwarded to the card. |