Web InnoventixFreeCode

Feature Icon Card

Original · free

A feature card led by an icon and short supporting copy.

byWeb InnoventixReact + Tailwind
cardfeatureiconcards
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/card-feature-icon.json
card-feature-icon.tsx
"use client";

import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Accent = "indigo" | "emerald" | "rose" | "amber" | "sky" | "violet";

type AccentSkin = {
  iconWrap: string;
  iconText: string;
  glow: string;
  badge: string;
  link: string;
  ring: string;
  pinOn: string;
};

const SKINS: Record<Accent, AccentSkin> = {
  indigo: {
    iconWrap:
      "bg-indigo-100 ring-indigo-200 dark:bg-indigo-500/15 dark:ring-indigo-400/20",
    iconText: "text-indigo-600 dark:text-indigo-300",
    glow: "from-indigo-400/25",
    badge:
      "bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20",
    link: "text-indigo-600 hover:text-indigo-700 dark:text-indigo-300 dark:hover:text-indigo-200",
    ring: "focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400",
    pinOn: "text-indigo-500 dark:text-indigo-300",
  },
  emerald: {
    iconWrap:
      "bg-emerald-100 ring-emerald-200 dark:bg-emerald-500/15 dark:ring-emerald-400/20",
    iconText: "text-emerald-600 dark:text-emerald-300",
    glow: "from-emerald-400/25",
    badge:
      "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/20",
    link: "text-emerald-600 hover:text-emerald-700 dark:text-emerald-300 dark:hover:text-emerald-200",
    ring: "focus-visible:ring-emerald-500 dark:focus-visible:ring-emerald-400",
    pinOn: "text-emerald-500 dark:text-emerald-300",
  },
  rose: {
    iconWrap:
      "bg-rose-100 ring-rose-200 dark:bg-rose-500/15 dark:ring-rose-400/20",
    iconText: "text-rose-600 dark:text-rose-300",
    glow: "from-rose-400/25",
    badge:
      "bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-500/10 dark:text-rose-300 dark:ring-rose-400/20",
    link: "text-rose-600 hover:text-rose-700 dark:text-rose-300 dark:hover:text-rose-200",
    ring: "focus-visible:ring-rose-500 dark:focus-visible:ring-rose-400",
    pinOn: "text-rose-500 dark:text-rose-300",
  },
  amber: {
    iconWrap:
      "bg-amber-100 ring-amber-200 dark:bg-amber-500/15 dark:ring-amber-400/20",
    iconText: "text-amber-600 dark:text-amber-300",
    glow: "from-amber-400/25",
    badge:
      "bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-400/20",
    link: "text-amber-600 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200",
    ring: "focus-visible:ring-amber-500 dark:focus-visible:ring-amber-400",
    pinOn: "text-amber-500 dark:text-amber-300",
  },
  sky: {
    iconWrap:
      "bg-sky-100 ring-sky-200 dark:bg-sky-500/15 dark:ring-sky-400/20",
    iconText: "text-sky-600 dark:text-sky-300",
    glow: "from-sky-400/25",
    badge:
      "bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-500/10 dark:text-sky-300 dark:ring-sky-400/20",
    link: "text-sky-600 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200",
    ring: "focus-visible:ring-sky-500 dark:focus-visible:ring-sky-400",
    pinOn: "text-sky-500 dark:text-sky-300",
  },
  violet: {
    iconWrap:
      "bg-violet-100 ring-violet-200 dark:bg-violet-500/15 dark:ring-violet-400/20",
    iconText: "text-violet-600 dark:text-violet-300",
    glow: "from-violet-400/25",
    badge:
      "bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/10 dark:text-violet-300 dark:ring-violet-400/20",
    link: "text-violet-600 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200",
    ring: "focus-visible:ring-violet-500 dark:focus-visible:ring-violet-400",
    pinOn: "text-violet-500 dark:text-violet-300",
  },
};

type IconKey = "bolt" | "shield" | "globe" | "rewind" | "gauge" | "branch";

function Icon({ name }: { name: IconKey }) {
  const common = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    "aria-hidden": true,
    className: "h-6 w-6",
  };
  switch (name) {
    case "bolt":
      return (
        <svg {...common}>
          <path d="M13 2 4.5 13.5H11l-1 8.5 8.5-11.5H12l1-8.5Z" />
        </svg>
      );
    case "shield":
      return (
        <svg {...common}>
          <path d="M12 3 5 6v5.5c0 4.3 2.9 7.6 7 9 4.1-1.4 7-4.7 7-9V6l-7-3Z" />
          <path d="m9.5 12 1.8 1.8L15 10" />
        </svg>
      );
    case "globe":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="9" />
          <path d="M3 12h18M12 3c2.5 2.4 3.8 5.6 3.8 9S14.5 18.6 12 21c-2.5-2.4-3.8-5.6-3.8-9S9.5 5.4 12 3Z" />
        </svg>
      );
    case "rewind":
      return (
        <svg {...common}>
          <path d="M11 19 4 12l7-7v4h3a6 6 0 1 1 0 12h-2" />
        </svg>
      );
    case "gauge":
      return (
        <svg {...common}>
          <path d="M12 13 16 9" />
          <path d="M4.5 17a9 9 0 1 1 15 0" />
          <circle cx="12" cy="14" r="1.4" fill="currentColor" stroke="none" />
        </svg>
      );
    case "branch":
      return (
        <svg {...common}>
          <circle cx="6" cy="5" r="2.2" />
          <circle cx="6" cy="19" r="2.2" />
          <circle cx="18" cy="8" r="2.2" />
          <path d="M6 7.2v9.6M8.2 8H14a1.8 1.8 0 0 1 1.8 1.8V6.2M15.8 10v0" />
          <path d="M8.2 8H13a3 3 0 0 0 3-3" />
        </svg>
      );
  }
}

type Feature = {
  id: string;
  accent: Accent;
  icon: IconKey;
  title: string;
  metric: string;
  copy: string;
  detail: string;
  points: string[];
  href: string;
};

const FEATURES: Feature[] = [
  {
    id: "edge",
    accent: "indigo",
    icon: "globe",
    title: "Global edge network",
    metric: "310 cities",
    copy: "Static assets and functions run from the location nearest each visitor, so pages feel instant everywhere.",
    detail:
      "Requests are routed to the closest of 310 points of presence with automatic failover. Cold-start times stay under 40ms even on the free tier.",
    points: [
      "Automatic multi-region failover",
      "Sub-40ms cold starts",
      "Zero-config asset caching",
    ],
    href: "#edge-network",
  },
  {
    id: "rollback",
    accent: "emerald",
    icon: "rewind",
    title: "Instant rollbacks",
    metric: "< 3s",
    copy: "Every deploy is immutable and addressable. Roll back to any previous build with a single click.",
    detail:
      "Because builds are content-addressed, restoring a prior version just re-points the alias. No rebuild, no downtime, no lost traffic.",
    points: [
      "Immutable, content-addressed builds",
      "One-click restore of any deploy",
      "Preserved environment variables",
    ],
    href: "#instant-rollback",
  },
  {
    id: "ddos",
    accent: "rose",
    icon: "shield",
    title: "Managed DDoS shield",
    metric: "24/7",
    copy: "Layer 3, 4 and 7 attacks are absorbed at the edge before they ever reach your origin servers.",
    detail:
      "Traffic is scored in real time and challenged when anomalous. Rules update automatically as new attack signatures appear worldwide.",
    points: [
      "Always-on L3/L4/L7 mitigation",
      "Adaptive bot scoring",
      "No traffic-based surcharges",
    ],
    href: "#ddos-shield",
  },
  {
    id: "analytics",
    accent: "sky",
    icon: "gauge",
    title: "Real-user analytics",
    metric: "p75 CWV",
    copy: "Core Web Vitals from actual visitors, broken down by route, device and country, with no cookies.",
    detail:
      "Sampled at 100% on Pro, analytics stream in within seconds of a page load so regressions surface before your customers report them.",
    points: [
      "Cookie-free, privacy-first collection",
      "Per-route LCP, INP and CLS",
      "Slack and email alerts on regressions",
    ],
    href: "#real-user-analytics",
  },
  {
    id: "preview",
    accent: "violet",
    icon: "branch",
    title: "Preview deployments",
    metric: "Every PR",
    copy: "Open a pull request and get a shareable, production-parity URL your whole team can review.",
    detail:
      "Each preview runs the same runtime as production with isolated environment variables, so what reviewers see is exactly what ships.",
    points: [
      "Production-parity runtime",
      "Isolated per-branch env vars",
      "Inline comments on the live URL",
    ],
    href: "#preview-deploys",
  },
  {
    id: "build",
    accent: "amber",
    icon: "bolt",
    title: "Incremental builds",
    metric: "8x faster",
    copy: "Only the files that changed are rebuilt, turning multi-minute deploys into a few seconds.",
    detail:
      "A persistent build cache fingerprints your dependency graph and reuses untouched output, so large monorepos deploy in seconds.",
    points: [
      "Fingerprinted dependency graph",
      "Shared cache across branches",
      "Parallelised across all cores",
    ],
    href: "#incremental-builds",
  },
];

function FeatureCard({ feature }: { feature: Feature }) {
  const skin = SKINS[feature.accent];
  const [open, setOpen] = useState(false);
  const [pinned, setPinned] = useState(false);
  const reduce = useReducedMotion();
  const panelId = useId();

  return (
    <article className="cfi-card group relative flex h-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-transform duration-300 will-change-transform hover:-translate-y-1 hover:shadow-xl dark:border-slate-800 dark:bg-slate-900">
      <span
        aria-hidden
        className={`pointer-events-none absolute -right-16 -top-16 h-40 w-40 rounded-full bg-gradient-to-br to-transparent opacity-0 blur-2xl transition-opacity duration-500 group-hover:opacity-100 ${skin.glow}`}
      />
      <span
        aria-hidden
        className="cfi-sheen pointer-events-none absolute inset-y-0 -left-1/3 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/40 to-transparent dark:via-white/10"
      />

      <div className="relative flex items-start justify-between gap-3">
        <span
          className={`cfi-icon inline-flex h-12 w-12 items-center justify-center rounded-xl ring-1 transition-transform duration-300 group-hover:-rotate-6 group-hover:scale-105 ${skin.iconWrap} ${skin.iconText}`}
        >
          <Icon name={feature.icon} />
        </span>

        <button
          type="button"
          onClick={() => setPinned((v) => !v)}
          aria-pressed={pinned}
          aria-label={pinned ? `Unpin ${feature.title}` : `Pin ${feature.title}`}
          className={`inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-slate-200 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900 ${skin.ring} ${
            pinned ? skin.pinOn : ""
          }`}
        >
          <svg
            viewBox="0 0 24 24"
            className="h-5 w-5"
            fill={pinned ? "currentColor" : "none"}
            stroke="currentColor"
            strokeWidth={1.75}
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden
          >
            <path d="m12 3 2.5 5.6 6.1.6-4.6 4 1.4 6-5.4-3.2L6.6 19l1.4-6L3.4 9l6.1-.6L12 3Z" />
          </svg>
        </button>
      </div>

      <div className="relative mt-5 flex items-center gap-2">
        <h3 className="text-base font-semibold text-slate-900 dark:text-white">
          {feature.title}
        </h3>
        <span
          className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ${skin.badge}`}
        >
          {feature.metric}
        </span>
      </div>

      <p className="relative mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
        {feature.copy}
      </p>

      <AnimatePresence initial={false}>
        {open && (
          <motion.div
            key="detail"
            id={panelId}
            initial={reduce ? false : { height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
            transition={{ duration: reduce ? 0 : 0.28, ease: [0.22, 1, 0.36, 1] }}
            className="relative overflow-hidden"
          >
            <p className="mt-4 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
              {feature.detail}
            </p>
            <ul className="mt-3 space-y-2">
              {feature.points.map((point) => (
                <li
                  key={point}
                  className="flex items-start gap-2 text-sm text-slate-700 dark:text-slate-300"
                >
                  <svg
                    viewBox="0 0 24 24"
                    className={`mt-0.5 h-4 w-4 shrink-0 ${skin.iconText}`}
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={2.25}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden
                  >
                    <path d="m5 12 4.5 4.5L19 7" />
                  </svg>
                  {point}
                </li>
              ))}
            </ul>
          </motion.div>
        )}
      </AnimatePresence>

      <div className="relative mt-5 flex items-center justify-between border-t border-slate-100 pt-4 dark:border-slate-800">
        <button
          type="button"
          onClick={() => setOpen((v) => !v)}
          aria-expanded={open}
          aria-controls={panelId}
          className={`inline-flex items-center gap-1 rounded-md text-sm font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${skin.link} ${skin.ring}`}
        >
          {open ? "Show less" : "Learn more"}
          <svg
            viewBox="0 0 24 24"
            className={`h-4 w-4 transition-transform duration-300 ${
              open ? "rotate-180" : ""
            }`}
            fill="none"
            stroke="currentColor"
            strokeWidth={2}
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden
          >
            <path d="m6 9 6 6 6-6" />
          </svg>
        </button>

        <a
          href={feature.href}
          className={`inline-flex items-center gap-1 rounded-md text-sm font-medium text-slate-500 transition-colors hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-white dark:focus-visible:ring-offset-slate-900 ${skin.ring}`}
        >
          Docs
          <svg
            viewBox="0 0 24 24"
            className="h-4 w-4"
            fill="none"
            stroke="currentColor"
            strokeWidth={2}
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden
          >
            <path d="M7 17 17 7M9 7h8v8" />
          </svg>
        </a>
      </div>
    </article>
  );
}

export default function CardFeatureIcon() {
  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 dark:bg-slate-950 sm:px-6 lg:px-8">
      <style>{`
        .cfi-scope .cfi-sheen { transform: translateX(0); opacity: 0; }
        .cfi-scope .cfi-card:hover .cfi-sheen {
          animation: cfi-sweep 0.9s ease forwards;
        }
        @keyframes cfi-sweep {
          0%   { transform: translateX(-40%); opacity: 0; }
          20%  { opacity: 1; }
          100% { transform: translateX(520%); opacity: 0; }
        }
        @keyframes cfi-rise {
          from { opacity: 0; transform: translateY(10px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        .cfi-scope .cfi-head { animation: cfi-rise 0.5s ease both; }
        @media (prefers-reduced-motion: reduce) {
          .cfi-scope .cfi-sheen,
          .cfi-scope .cfi-card:hover .cfi-sheen,
          .cfi-scope .cfi-head { animation: none !important; }
        }
      `}</style>

      <div className="cfi-scope mx-auto max-w-6xl">
        <div className="cfi-head 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 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
            Platform features
          </span>
          <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Everything you need to ship fast
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            A production-grade deployment platform, built for teams who care
            about speed, safety and the details in between.
          </p>
        </div>

        <div className="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {FEATURES.map((feature) => (
            <FeatureCard key={feature.id} feature={feature} />
          ))}
        </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 →