liten

Bento Card

A feature-grid tile built on a raised panel, a recessed stage for the visual, and a scroll-in reveal.

New
Trigger
Condition
Action
Run passed

Visual workflow builder

Drag triggers, conditions, and actions into place. Ship automations in minutes, not sprints.

Learn more
Resolved

Support that answers itself

AI agents resolve tickets around the clock.

Oct 21$6.1k

Revenue in real time

Every metric current, every chart live.

View dashboard
Sign in
Encrypted

Secure 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.

Terminal
bun add motion

Copy the component into components/ui/bento-card.tsx.

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

Example.tsx
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

PropTypeDefaultDescription
childrenReact.ReactNode-BentoCard elements to lay out.
columns2 | 3 | 43Columns at the largest breakpoint.
classNamestring-Forwarded to the grid.

BentoCard

PropTypeDefaultDescription
titlestring-Card title.
descriptionstring-Card description.
iconReact.ReactNode-A lucide icon rendered in a raised icon chip.
badgestring-Accent tint pill in the top-right corner.
span1 | 2 | 31Column span from the sm breakpoint up.
hrefstring-Renders the card as a link; only links lift.
ctastring"Learn more"Label for the link row shown when href is set.
accentstring"#f0883e"Focus ring + badge tint.
fadebooleantrueFade the bottom of the visual into the card.
delaynumber-Reveal delay in seconds; BentoGrid sets this.
childrenReact.ReactNode-The visual, rendered in the recessed stage.
classNamestring-Forwarded to the card.
On this page0%