Web InnoventixFreeCode

Bento Feature Section

Original · free

bento feature grid

byWeb InnoventixReact + Tailwind
featxbentofeatures
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/featx-bento.json
featx-bento.tsx
"use client";

import { useId, useRef, useState } from "react";
import type { KeyboardEvent, ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";

type Metric = {
  id: string;
  label: string;
  value: string;
  delta: string;
  caption: string;
  bars: number[];
};

const METRICS: Metric[] = [
  {
    id: "revenue",
    label: "Revenue",
    value: "$482,900",
    delta: "+12.4%",
    caption: "Monthly recurring revenue, refreshed the second a charge clears.",
    bars: [38, 52, 46, 61, 70, 64, 88],
  },
  {
    id: "signups",
    label: "Signups",
    value: "9,214",
    delta: "+6.1%",
    caption: "New workspaces created across every plan this month.",
    bars: [28, 40, 36, 54, 60, 73, 82],
  },
  {
    id: "retention",
    label: "Retention",
    value: "94.2%",
    delta: "+1.8%",
    caption: "Accounts still active ninety days after they signed up.",
    bars: [70, 66, 74, 72, 79, 83, 91],
  },
];

const FEATURES: { title: string; body: string; icon: ReactNode; iconWrap: string }[] = [
  {
    title: "Alerts that reach you first",
    body: "Route anomalies to chat, email, or a signed webhook the moment a metric crosses the line you set.",
    iconWrap: "bg-rose-500/10 text-rose-600 dark:text-rose-400",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
        <path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
        <path d="M13.73 21a2 2 0 0 1-3.46 0" />
      </svg>
    ),
  },
  {
    title: "Access down to the field",
    body: "Role-based permissions scoped per column, so contractors see revenue but never customer PII.",
    iconWrap: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
        <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
        <path d="m9 12 2 2 4-4" />
      </svg>
    ),
  },
  {
    title: "Sub-second on billions of rows",
    body: "Columnar storage and a vectorized engine answer queries across billions of rows in well under a second.",
    iconWrap: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
        <path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z" />
      </svg>
    ),
  },
];

const INTEGRATIONS: { label: string; icon: ReactNode }[] = [
  {
    label: "Warehouse",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <ellipse cx="12" cy="5" rx="8" ry="3" />
        <path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5" />
        <path d="M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
      </svg>
    ),
  },
  {
    label: "CRM",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
        <circle cx="9" cy="7" r="4" />
        <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
      </svg>
    ),
  },
  {
    label: "Payments",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <rect x="2" y="5" width="20" height="14" rx="2" />
        <path d="M2 10h20" />
      </svg>
    ),
  },
  {
    label: "Email",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <rect x="2" y="4" width="20" height="16" rx="2" />
        <path d="m4 6 8 6 8-6" />
      </svg>
    ),
  },
  {
    label: "Webhooks",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1" />
        <path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1" />
      </svg>
    ),
  },
  {
    label: "REST API",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="m8 6-6 6 6 6" />
        <path d="m16 6 6 6-6 6" />
      </svg>
    ),
  },
];

const KEYFRAMES = `
@keyframes featx-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.62); }
}
@keyframes featx-sweep {
  0% { transform: translateX(-140%); }
  100% { transform: translateX(420%); }
}
@keyframes featx-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-8px); }
}
@media (prefers-reduced-motion: reduce) {
  .featx-anim { animation: none !important; }
}
`;

export default function FeatxBento() {
  const reduce = useReducedMotion();
  const uid = useId();
  const [activeId, setActiveId] = useState<string>(METRICS[0].id);
  const [autoOn, setAutoOn] = useState<boolean>(true);
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const active = METRICS.find((m) => m.id === activeId) ?? METRICS[0];

  const rise = (i: number) =>
    reduce
      ? {}
      : {
          initial: { opacity: 0, y: 24 },
          whileInView: { opacity: 1, y: 0 },
          viewport: { once: true, margin: "-80px" },
          transition: { duration: 0.5, delay: i * 0.06, ease: "easeOut" as const },
        };

  const onTabKey = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
    const count = METRICS.length;
    let next = index;
    if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (index + 1) % count;
    else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (index - 1 + count) % count;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = count - 1;
    else return;
    e.preventDefault();
    setActiveId(METRICS[next].id);
    tabRefs.current[next]?.focus();
  };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 py-20 dark:bg-slate-950 sm:py-28">
      <style>{KEYFRAMES}</style>

      <div className="pointer-events-none absolute inset-0">
        <div className="featx-anim absolute left-1/2 top-[-6rem] h-[420px] w-[420px] -translate-x-1/2 rounded-full bg-indigo-400/10 blur-3xl [animation:featx-float_9s_ease-in-out_infinite] dark:bg-indigo-500/10" />
      </div>

      <div className="relative z-10 mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
        <div className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
            <span className="inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
            The Nimbus platform
          </span>
          <h2 className="mt-5 text-balance text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Everything your team needs to move faster
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400 sm:text-lg">
            Nimbus unifies analytics, automation, and alerting in one workspace — so your team ships decisions
            instead of maintaining spreadsheets.
          </p>
        </div>

        <div className="mt-14 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-6 lg:auto-rows-[200px]">
          {/* Hero — interactive metric tabs */}
          <motion.div
            {...rise(0)}
            className="group relative flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900 md:col-span-2 lg:col-span-4 lg:row-span-2"
          >
            <div className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-gradient-to-br from-indigo-500/20 to-violet-500/10 blur-2xl" />

            <div className="relative">
              <div className="flex items-center gap-2">
                <span className="featx-anim inline-block h-2 w-2 rounded-full bg-emerald-500 [animation:featx-pulse_1.8s_ease-in-out_infinite]" />
                <span className="text-xs font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
                  Live
                </span>
              </div>
              <h3 className="mt-3 text-lg font-semibold text-slate-900 dark:text-white">
                See every metric update the moment it happens
              </h3>
            </div>

            <div
              role="tablist"
              aria-label="Choose a metric to preview"
              className="relative mt-5 flex gap-1 rounded-lg bg-slate-100 p-1 dark:bg-slate-800/70"
            >
              {METRICS.map((m, i) => {
                const selected = m.id === activeId;
                return (
                  <button
                    key={m.id}
                    ref={(el) => {
                      tabRefs.current[i] = el;
                    }}
                    role="tab"
                    type="button"
                    id={`${uid}-tab-${m.id}`}
                    aria-selected={selected}
                    aria-controls={`${uid}-panel`}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => setActiveId(m.id)}
                    onKeyDown={(e) => onTabKey(e, i)}
                    className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
                      selected
                        ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
                        : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                    }`}
                  >
                    {m.label}
                  </button>
                );
              })}
            </div>

            <div
              id={`${uid}-panel`}
              role="tabpanel"
              aria-labelledby={`${uid}-tab-${active.id}`}
              className="relative mt-5 flex flex-1 flex-col"
            >
              <div className="flex items-end gap-3">
                <span className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
                  {active.value}
                </span>
                <span className="mb-1 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
                  {active.delta}
                </span>
              </div>
              <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{active.caption}</p>

              <div className="mt-auto flex h-28 items-end gap-2 pt-6">
                {active.bars.map((b, idx) => (
                  <motion.div
                    key={`${active.id}-${idx}`}
                    className="flex-1 rounded-t-md bg-gradient-to-t from-indigo-500 to-violet-400 dark:from-indigo-500 dark:to-violet-400"
                    style={{ minWidth: 0 }}
                    initial={reduce ? false : { height: 0 }}
                    animate={{ height: `${b}%` }}
                    transition={reduce ? { duration: 0 } : { duration: 0.6, delay: idx * 0.05, ease: "easeOut" }}
                  />
                ))}
              </div>
            </div>
          </motion.div>

          {/* Automations — accessible toggle */}
          <motion.div
            {...rise(1)}
            className="relative flex flex-col justify-between overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900 lg:col-span-2"
          >
            <div>
              <div className="flex items-start justify-between">
                <span className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
                    <path d="M21 12a9 9 0 1 1-2.64-6.36" />
                    <path d="M21 3v5h-5" />
                  </svg>
                </span>
                <button
                  type="button"
                  role="switch"
                  aria-checked={autoOn}
                  aria-label="Toggle automations"
                  onClick={() => setAutoOn((v) => !v)}
                  className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
                    autoOn ? "bg-indigo-500" : "bg-slate-300 dark:bg-slate-700"
                  }`}
                >
                  <span
                    className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
                      autoOn ? "translate-x-5" : "translate-x-0.5"
                    }`}
                  />
                </button>
              </div>
              <h3 className="mt-4 text-base font-semibold text-slate-900 dark:text-white">Automations</h3>
              <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
                Trigger workflows the instant a threshold is crossed — no code, no cron jobs.
              </p>
            </div>
            <div className="mt-4 flex items-center gap-2 text-sm font-medium">
              <span className={`inline-block h-2 w-2 rounded-full ${autoOn ? "bg-emerald-500" : "bg-slate-400"}`} />
              <span className={autoOn ? "text-emerald-600 dark:text-emerald-400" : "text-slate-500 dark:text-slate-400"}>
                {autoOn ? "3 flows active" : "All flows paused"}
              </span>
            </div>
          </motion.div>

          {/* Throughput stat */}
          <motion.div
            {...rise(2)}
            className="relative flex flex-col justify-between overflow-hidden rounded-2xl border border-slate-700 bg-gradient-to-br from-slate-900 to-slate-800 p-6 text-white lg:col-span-2"
          >
            <div className="pointer-events-none absolute inset-0 overflow-hidden">
              <div className="featx-anim absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/10 to-transparent [animation:featx-sweep_3.5s_ease-in-out_infinite]" />
            </div>
            <div className="relative flex items-center gap-2">
              <span className="featx-anim inline-block h-2 w-2 rounded-full bg-emerald-400 [animation:featx-pulse_1.8s_ease-in-out_infinite]" />
              <span className="text-xs font-semibold uppercase tracking-wide text-slate-300">Processing now</span>
            </div>
            <div className="relative">
              <div className="text-4xl font-bold tracking-tight">2.4M</div>
              <p className="mt-1 text-sm text-slate-300">
                events per second, ingested and queryable in under 200 milliseconds.
              </p>
            </div>
          </motion.div>

          {/* Three feature cards */}
          {FEATURES.map((f, i) => (
            <motion.div
              key={f.title}
              {...rise(3 + i)}
              className="group relative flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 transition-colors hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700 lg:col-span-2"
            >
              <span className={`flex h-10 w-10 items-center justify-center rounded-lg ${f.iconWrap}`}>{f.icon}</span>
              <h3 className="mt-4 text-base font-semibold text-slate-900 dark:text-white">{f.title}</h3>
              <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{f.body}</p>
            </motion.div>
          ))}

          {/* Integrations — full width band */}
          <motion.div
            {...rise(6)}
            className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900 md:col-span-2 lg:col-span-6 sm:p-8"
          >
            <div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
              <div className="max-w-md">
                <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
                  Plugs into the stack you already run
                </h3>
                <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
                  Six hundred and counting connectors, or build your own with a signed webhook and our typed REST API.
                </p>
              </div>
              <div className="grid grid-cols-3 gap-3 sm:grid-cols-6">
                {INTEGRATIONS.map((it) => (
                  <div
                    key={it.label}
                    className="group flex flex-col items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-4 text-center transition-all hover:-translate-y-0.5 hover:border-indigo-300 hover:shadow-sm dark:border-slate-800 dark:bg-slate-800/40 dark:hover:border-indigo-500/50"
                  >
                    <span className="text-slate-500 transition-colors group-hover:text-indigo-600 dark:text-slate-400 dark:group-hover:text-indigo-400">
                      {it.icon}
                    </span>
                    <span className="text-xs font-medium text-slate-600 dark:text-slate-300">{it.label}</span>
                  </div>
                ))}
              </div>
            </div>
          </motion.div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →
Three Column Icon Grid

Three Column Icon Grid

Original

A responsive icon grid that presents six product capabilities as scannable cards, each with an inline SVG icon, title and short description.

Alternating Media Rows

Alternating Media Rows

Original

Three feature rows that alternate a text column with a CSS-built visual panel, walking the reader through a plan, measure and ship story.

Bento Feature Grid

Bento Feature Grid

Original

A bento-style grid mixing one large hero cell, a tall stat cell and several compact cards to give features a clear visual hierarchy.

Tabbed Feature Switcher

Tabbed Feature Switcher

Original

A tabbed feature preview that switches panels using only native radio inputs and Tailwind peer variants, so it works with no JavaScript.

Stat Backed Features

Stat Backed Features

Original

A feature layout that pairs each capability with a headline metric, so every claim is anchored to a measurable result.

Scroll-reveal bento grid

Scroll-reveal bento grid

Original

An asymmetric bento feature grid whose cards stagger into view on scroll, lift on hover and feature an animated conic gradient border, shimmering headline, growing bar chart and a masked marquee tag strip.

Sticky scroll steps

Sticky scroll steps

Original

A scroll-linked how-it-works section where a sticky panel cross-fades between steps and a progress ring plus filling timeline track advance as you scroll through the process.

Spotlight hover feature cards

Spotlight hover feature cards

Original

A responsive feature card grid where each card follows the cursor with a radial spotlight glow, lifts and rotates its icon on hover, reveals a call to action, and sits above a scrolling logo marquee.

Animated marquee highlights band

Animated marquee highlights band

Original

A bold feature band with an animated gradient background, floating blurred orbs, blur-in staggered headline, two opposing marquee pill rows and shimmering stat cards.

Bordered Feature Grid (2x2)

Bordered Feature Grid (2x2)

MIT

A two-column grid of bordered feature cards, each pairing an outlined icon with a title and supporting copy. Clean, enterprise-leaning layout with full dark-mode support.

Outlined Feature Cards with CTA

Outlined Feature Cards with CTA

MIT

A three-column feature section with heading and intro, plus blue-outlined cards that each carry an icon, description, and a circular arrow call-to-action. Fully theme-aware.

Icon-Left Feature List

Icon-Left Feature List

MIT

A centered heading over a three-column feature list, each row leading with a rounded icon badge and a 'Learn More' link. Classic marketing feature block, ported with added dark variants.