Web InnoventixFreeCode

Gradient Mesh Hero

Original · free

hero on an animated mesh gradient

byWeb InnoventixReact + Tailwind
heroxgradientmeshheroes
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/herox-gradient-mesh.json
herox-gradient-mesh.tsx
"use client";

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

type FeatureTab = {
  id: string;
  label: string;
  headline: string;
  metric: string;
  metricLabel: string;
  points: string[];
  bars: number[];
};

const TABS: FeatureTab[] = [
  {
    id: "automate",
    label: "Automate",
    headline: "Automate the busywork",
    metric: "8.5 hrs",
    metricLabel: "saved per teammate every week",
    points: [
      "Turn repeatable steps into workflows that run on their own",
      "Trigger on any event across the tools you already use",
      "Approvals, retries and rollbacks handled for you",
    ],
    bars: [42, 58, 51, 73, 66, 88, 79],
  },
  {
    id: "analyze",
    label: "Analyze",
    headline: "See every workflow live",
    metric: "312 ms",
    metricLabel: "median run time across pipelines",
    points: [
      "A real-time map of every run, queue and dependency",
      "Alerts the moment a workflow slows or drifts off target",
      "Drill from a single failed step to its exact input",
    ],
    bars: [30, 44, 40, 55, 62, 58, 71],
  },
  {
    id: "scale",
    label: "Scale",
    headline: "Scale without new headcount",
    metric: "6×",
    metricLabel: "throughput per engineer, on average",
    points: [
      "Run thousands of concurrent workflows on shared infra",
      "Cost controls and budgets enforced before anything ships",
      "Grow from one team to the whole org on the same setup",
    ],
    bars: [48, 52, 64, 60, 78, 84, 95],
  },
];

const STATS: { value: string; label: string }[] = [
  { value: "12k+", label: "teams onboard" },
  { value: "40M", label: "workflows / month" },
  { value: "99.99%", label: "uptime SLA" },
];

const BLOBS: {
  className: string;
  anim: string;
  duration: string;
  delay: string;
}[] = [
  {
    className:
      "left-[-12%] top-[-14%] h-[46rem] w-[46rem] bg-indigo-400/60 dark:bg-indigo-500/40",
    anim: "heroxmesh-drift-1",
    duration: "22s",
    delay: "0s",
  },
  {
    className:
      "right-[-14%] top-[-8%] h-[40rem] w-[40rem] bg-violet-400/55 dark:bg-violet-600/40",
    anim: "heroxmesh-drift-2",
    duration: "26s",
    delay: "-6s",
  },
  {
    className:
      "left-[22%] bottom-[-24%] h-[44rem] w-[44rem] bg-sky-400/55 dark:bg-sky-500/35",
    anim: "heroxmesh-drift-3",
    duration: "30s",
    delay: "-3s",
  },
  {
    className:
      "right-[6%] bottom-[-18%] h-[34rem] w-[34rem] bg-emerald-300/55 dark:bg-emerald-500/30",
    anim: "heroxmesh-drift-1",
    duration: "28s",
    delay: "-11s",
  },
  {
    className:
      "left-[40%] top-[8%] h-[30rem] w-[30rem] bg-rose-300/50 dark:bg-rose-500/25",
    anim: "heroxmesh-drift-2",
    duration: "24s",
    delay: "-14s",
  },
];

function isValidEmail(value: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
}

export default function HeroxGradientMesh() {
  const reduce = useReducedMotion();
  const uid = useId();
  const [active, setActive] = useState<number>(0);
  const [email, setEmail] = useState<string>("");
  const [status, setStatus] = useState<"idle" | "error" | "done">("idle");
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const activeTab = TABS[active];

  function focusTab(index: number) {
    const next = (index + TABS.length) % TABS.length;
    setActive(next);
    tabRefs.current[next]?.focus();
  }

  function onTabKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
    if (event.key === "ArrowRight" || event.key === "ArrowDown") {
      event.preventDefault();
      focusTab(index + 1);
    } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
      event.preventDefault();
      focusTab(index - 1);
    } else if (event.key === "Home") {
      event.preventDefault();
      focusTab(0);
    } else if (event.key === "End") {
      event.preventDefault();
      focusTab(TABS.length - 1);
    }
  }

  function onEmailChange(event: ChangeEvent<HTMLInputElement>) {
    setEmail(event.target.value);
    if (status !== "idle") setStatus("idle");
  }

  function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!isValidEmail(email)) {
      setStatus("error");
      return;
    }
    setStatus("done");
  }

  const ease = [0.22, 1, 0.36, 1] as const;

  return (
    <section className="relative w-full overflow-hidden bg-white px-5 py-24 text-slate-900 sm:px-8 sm:py-28 lg:py-36 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes heroxmesh-drift-1 {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          33% { transform: translate3d(7%, -9%, 0) scale(1.16); }
          66% { transform: translate3d(-6%, 7%, 0) scale(0.9); }
        }
        @keyframes heroxmesh-drift-2 {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
          40% { transform: translate3d(-9%, 8%, 0) scale(0.88); }
          75% { transform: translate3d(6%, -5%, 0) scale(1.2); }
        }
        @keyframes heroxmesh-drift-3 {
          0%, 100% { transform: translate3d(0, 0, 0) scale(0.95); }
          50% { transform: translate3d(9%, -7%, 0) scale(1.18); }
        }
        @keyframes heroxmesh-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-10px); }
        }
        @keyframes heroxmesh-rise {
          from { transform: scaleY(0.35); opacity: 0.35; }
          to { transform: scaleY(1); opacity: 1; }
        }
        @keyframes heroxmesh-shimmer {
          from { background-position: 200% 0; }
          to { background-position: -200% 0; }
        }
        .heroxmesh-blob { animation: var(--heroxmesh-anim) var(--heroxmesh-dur) ease-in-out infinite; animation-delay: var(--heroxmesh-delay); will-change: transform; }
        .heroxmesh-card { animation: heroxmesh-float 7s ease-in-out infinite; }
        .heroxmesh-bar { transform-origin: bottom; animation: heroxmesh-rise 0.7s cubic-bezier(0.22,1,0.36,1) both; }
        @media (prefers-reduced-motion: reduce) {
          .heroxmesh-blob, .heroxmesh-card, .heroxmesh-bar { animation: none !important; }
        }
      `}</style>

      {/* Animated mesh gradient */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
        {BLOBS.map((blob, i) => (
          <div
            key={i}
            className={`heroxmesh-blob absolute rounded-full blur-3xl mix-blend-multiply dark:mix-blend-screen ${blob.className}`}
            style={
              {
                "--heroxmesh-anim": blob.anim,
                "--heroxmesh-dur": blob.duration,
                "--heroxmesh-delay": blob.delay,
              } as Record<string, string>
            }
          />
        ))}
        {/* Fine grid overlay */}
        <div
          className="absolute inset-0 opacity-[0.05] dark:opacity-[0.08]"
          style={{
            backgroundImage:
              "linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)",
            backgroundSize: "44px 44px",
          }}
        />
        {/* Readability wash */}
        <div className="absolute inset-0 bg-gradient-to-b from-white/40 via-transparent to-white/70 dark:from-slate-950/50 dark:via-transparent dark:to-slate-950/80" />
      </div>

      <div className="mx-auto grid max-w-7xl items-center gap-14 lg:grid-cols-[1.05fr_0.95fr] lg:gap-16">
        {/* Left: copy + CTA */}
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 24 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.7, ease }}
        >
          <a
            href="#changelog"
            className="group inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3.5 py-1.5 text-xs font-medium text-slate-700 shadow-sm backdrop-blur transition-colors hover:border-indigo-300 hover:text-indigo-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:border-indigo-400/50 dark:hover:text-indigo-300"
          >
            <span className="inline-flex h-4 items-center rounded-full bg-indigo-600 px-1.5 text-[10px] font-semibold uppercase tracking-wide text-white dark:bg-indigo-500">
              New
            </span>
            Nimbus Agents are now generally available
            <svg
              viewBox="0 0 24 24"
              className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5"
              fill="none"
              stroke="currentColor"
              strokeWidth={2}
              strokeLinecap="round"
              strokeLinejoin="round"
              aria-hidden="true"
            >
              <path d="M5 12h14M13 6l6 6-6 6" />
            </svg>
          </a>

          <h1 className="mt-6 text-4xl font-semibold tracking-tight text-slate-900 sm:text-5xl lg:text-6xl dark:text-white">
            Ship operations that
            <span className="relative whitespace-nowrap">
              {" "}
              <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-sky-400">
                run themselves
              </span>
            </span>
            .
          </h1>

          <p className="mt-6 max-w-xl text-lg leading-relaxed text-slate-600 dark:text-slate-300">
            Nimbus connects your stack, watches every workflow, and clears the
            busywork before it ever reaches a person. Set it up once and let the
            platform handle the rest.
          </p>

          {/* Email capture */}
          <form onSubmit={onSubmit} noValidate className="mt-8 max-w-md">
            <label htmlFor={`${uid}-email`} className="sr-only">
              Work email
            </label>
            <div className="flex flex-col gap-3 sm:flex-row">
              <div className="relative flex-1">
                <span className="pointer-events-none absolute inset-y-0 left-3.5 flex items-center text-slate-400 dark:text-slate-500">
                  <svg
                    viewBox="0 0 24 24"
                    className="h-4.5 w-4.5"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={2}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden="true"
                  >
                    <rect x="3" y="5" width="18" height="14" rx="2" />
                    <path d="m3 7 9 6 9-6" />
                  </svg>
                </span>
                <input
                  id={`${uid}-email`}
                  type="email"
                  inputMode="email"
                  autoComplete="email"
                  value={email}
                  onChange={onEmailChange}
                  placeholder="you@company.com"
                  aria-invalid={status === "error"}
                  aria-describedby={`${uid}-status`}
                  className="w-full rounded-xl border border-slate-300 bg-white/80 py-3 pl-10 pr-3.5 text-sm text-slate-900 shadow-sm outline-none backdrop-blur transition focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/40 dark:border-white/10 dark:bg-white/5 dark:text-white dark:placeholder:text-slate-500 dark:focus:border-indigo-400"
                />
              </div>
              <button
                type="submit"
                className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 active:scale-[0.98] dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200"
              >
                Start free
                <svg
                  viewBox="0 0 24 24"
                  className="h-4 w-4"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden="true"
                >
                  <path d="M5 12h14M13 6l6 6-6 6" />
                </svg>
              </button>
            </div>
            <p
              id={`${uid}-status`}
              aria-live="polite"
              className={`mt-2.5 min-h-[1.25rem] text-sm ${
                status === "error"
                  ? "text-rose-600 dark:text-rose-400"
                  : status === "done"
                    ? "text-emerald-600 dark:text-emerald-400"
                    : "text-slate-500 dark:text-slate-400"
              }`}
            >
              {status === "error"
                ? "Enter a valid work email to continue."
                : status === "done"
                  ? "You're on the list — check your inbox to activate."
                  : "No credit card · SOC 2 Type II · 14-day trial"}
            </p>
          </form>

          {/* Stats */}
          <dl className="mt-10 flex flex-wrap gap-x-10 gap-y-5">
            {STATS.map((stat) => (
              <div key={stat.label} className="flex flex-col">
                <dt className="order-2 text-sm text-slate-500 dark:text-slate-400">
                  {stat.label}
                </dt>
                <dd className="order-1 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
                  {stat.value}
                </dd>
              </div>
            ))}
          </dl>
        </motion.div>

        {/* Right: interactive preview card */}
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 28, scale: 0.98 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          transition={{ duration: 0.8, ease, delay: 0.1 }}
          className="relative"
        >
          <div className="heroxmesh-card rounded-3xl border border-slate-200/80 bg-white/70 p-5 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-6 dark:border-white/10 dark:bg-slate-900/60 dark:shadow-black/30">
            {/* Window chrome */}
            <div className="mb-5 flex items-center gap-2">
              <span className="h-3 w-3 rounded-full bg-rose-400" />
              <span className="h-3 w-3 rounded-full bg-amber-400" />
              <span className="h-3 w-3 rounded-full bg-emerald-400" />
              <span className="ml-3 text-xs font-medium text-slate-400 dark:text-slate-500">
                nimbus · live workspace
              </span>
            </div>

            {/* Tabs */}
            <div
              role="tablist"
              aria-label="Product capabilities"
              className="flex gap-1 rounded-xl bg-slate-100 p-1 dark:bg-white/5"
            >
              {TABS.map((tab, i) => {
                const selected = i === active;
                return (
                  <button
                    key={tab.id}
                    ref={(el) => {
                      tabRefs.current[i] = el;
                    }}
                    role="tab"
                    id={`${uid}-tab-${tab.id}`}
                    aria-selected={selected}
                    aria-controls={`${uid}-panel-${tab.id}`}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => setActive(i)}
                    onKeyDown={(e) => onTabKeyDown(e, i)}
                    className={`relative flex-1 rounded-lg px-3 py-2 text-sm font-medium transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
                      selected
                        ? "text-slate-900 dark:text-white"
                        : "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
                    }`}
                  >
                    {selected && (
                      <motion.span
                        layoutId={`${uid}-tabpill`}
                        className="absolute inset-0 rounded-lg bg-white shadow-sm dark:bg-slate-800"
                        transition={{ duration: reduce ? 0 : 0.3, ease }}
                      />
                    )}
                    <span className="relative">{tab.label}</span>
                  </button>
                );
              })}
            </div>

            {/* Panel */}
            <div
              role="tabpanel"
              id={`${uid}-panel-${activeTab.id}`}
              aria-labelledby={`${uid}-tab-${activeTab.id}`}
              className="mt-5"
            >
              <AnimatePresence mode="wait">
                <motion.div
                  key={activeTab.id}
                  initial={reduce ? false : { opacity: 0, y: 10 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -10 }}
                  transition={{ duration: 0.35, ease }}
                >
                  <div className="flex items-end justify-between gap-4">
                    <div>
                      <h2 className="text-base font-semibold text-slate-900 dark:text-white">
                        {activeTab.headline}
                      </h2>
                      <div className="mt-3 flex items-baseline gap-2">
                        <span className="bg-gradient-to-r from-indigo-600 to-violet-600 bg-clip-text text-4xl font-bold tracking-tight text-transparent dark:from-indigo-400 dark:to-violet-400">
                          {activeTab.metric}
                        </span>
                      </div>
                      <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
                        {activeTab.metricLabel}
                      </p>
                    </div>

                    {/* Mini bar chart */}
                    <div
                      className="flex h-24 items-end gap-1.5"
                      aria-hidden="true"
                    >
                      {activeTab.bars.map((h, bi) => (
                        <div
                          key={bi}
                          className="heroxmesh-bar w-3 rounded-t bg-gradient-to-t from-indigo-500/70 to-violet-400 dark:from-indigo-500/60 dark:to-violet-300"
                          style={{
                            height: `${h}%`,
                            animationDelay: reduce ? "0s" : `${bi * 60}ms`,
                          }}
                        />
                      ))}
                    </div>
                  </div>

                  <ul className="mt-5 space-y-2.5">
                    {activeTab.points.map((point) => (
                      <li
                        key={point}
                        className="flex items-start gap-2.5 text-sm text-slate-600 dark:text-slate-300"
                      >
                        <svg
                          viewBox="0 0 24 24"
                          className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500 dark:text-emerald-400"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={2.4}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                          aria-hidden="true"
                        >
                          <path d="M20 6 9 17l-5-5" />
                        </svg>
                        {point}
                      </li>
                    ))}
                  </ul>
                </motion.div>
              </AnimatePresence>
            </div>

            {/* Footer action */}
            <div className="mt-6 flex items-center justify-between border-t border-slate-200 pt-4 dark:border-white/10">
              <span className="text-xs text-slate-400 dark:text-slate-500">
                Updated moments ago
              </span>
              <a
                href="#demo"
                className="inline-flex items-center gap-1.5 text-sm font-semibold text-indigo-600 transition hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
              >
                Book a demo
                <svg
                  viewBox="0 0 24 24"
                  className="h-4 w-4"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden="true"
                >
                  <path d="M5 12h14M13 6l6 6-6 6" />
                </svg>
              </a>
            </div>
          </div>

          {/* Floating badge */}
          <div className="absolute -bottom-4 -left-4 hidden rounded-2xl border border-slate-200/80 bg-white/80 px-4 py-3 shadow-lg backdrop-blur-xl sm:block dark:border-white/10 dark:bg-slate-900/70">
            <div className="flex items-center gap-2.5">
              <span className="relative flex h-2.5 w-2.5">
                <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
                <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
              </span>
              <div>
                <p className="text-sm font-semibold text-slate-900 dark:text-white">
                  All systems operational
                </p>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  1,284 workflows running now
                </p>
              </div>
            </div>
          </div>
        </motion.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 →
Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero

Image Backdrop Hero

Original

A cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal

Gradient Mesh Hero with Staggered Text Reveal

Original

A full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

Typewriter Rotating Words Hero

Typewriter Rotating Words Hero

Original

A hero whose headline cycles through rotating words with a live typewriter caret over an animated grid backdrop, degrading to a gentle word swap when reduced motion is on.

Floating UI Cards Hero with Mouse Parallax

Floating UI Cards Hero with Mouse Parallax

Original

A hero with glassy floating UI cards that drift continuously and shift on mouse-parallax depth around a staggered centre headline.

Aurora Hero with Shimmer CTA and Logo Marquee

Aurora Hero with Shimmer CTA and Logo Marquee

Original

A dark hero with animated aurora light beams, a shimmering sweep across the primary CTA, and dual-direction logo marquees beneath the fold.

Gradient Conversion Hero

Gradient Conversion Hero

MIT

Centered, full-screen marketing hero with a colour-accent headline word, supporting copy, and dual primary/secondary CTA buttons. Dark-mode ready and dependency-free.

Split Showcase Hero

Split Showcase Hero

MIT

Two-column hero on a dark background with a headline, paragraph, and two buttons beside a gradient media panel. Responsive column stack on mobile.