Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 03:13:37Get Lifetime →
loopkit
freeNew130 lines · framer-motion

Icon Feature Grid

A clean icon-and-text grid that tilts toward you on hover. A responsive grid of feature cards, each with a hand-drawn line icon in a tinted badge, a title and a short description, with a subtle 3D tilt on hover.

Use it for
A straightforward features section where the point is clarity and speed to scan, not heavy visuals — docs pages, pricing pages, about pages.
Themes
Light + dark
License
Commercial use
Live

Why Loopkit

Everything an agent app needs, nothing it doesn't

Fast by default

Sub-100ms responses, no tuning required.

Secure at the core

Encrypted at rest and in transit, always.

Composable layers

Mix and match pieces as your stack grows.

Clear reporting

Know what happened and why, in one place.

Global by design

Edge nodes on every continent you ship to.

Always on time

Scheduled jobs that actually run on schedule.

tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";

function useReducedMotionSafe() {
  const prefersReduced = useReducedMotion();
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return mounted ? !!prefersReduced : false;
}

export interface IconFeature {
  title: string;
  description: string;
  icon?: "bolt" | "shield" | "layers" | "chart" | "globe" | "clock";
}

export interface IconFeatureGridProps {
  eyebrow?: string;
  title?: string;
  features?: IconFeature[];
  /** Accepts a number or a numeric string (controls panels pass strings). */
  columns?: 2 | 3 | 4 | "2" | "3" | "4";
  className?: string;
}

const DEFAULT_FEATURES: IconFeature[] = [
  { title: "Fast by default", description: "Sub-100ms responses, no tuning required.", icon: "bolt" },
  { title: "Secure at the core", description: "Encrypted at rest and in transit, always.", icon: "shield" },
  { title: "Composable layers", description: "Mix and match pieces as your stack grows.", icon: "layers" },
  { title: "Clear reporting", description: "Know what happened and why, in one place.", icon: "chart" },
  { title: "Global by design", description: "Edge nodes on every continent you ship to.", icon: "globe" },
  { title: "Always on time", description: "Scheduled jobs that actually run on schedule.", icon: "clock" },
];

function Icon({ name }: { name: NonNullable<IconFeature["icon"]> }) {
  const common = { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
  switch (name) {
    case "bolt":
      return (
        <svg {...common} className="size-6">
          <path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" />
        </svg>
      );
    case "shield":
      return (
        <svg {...common} className="size-6">
          <path d="M12 3 4 6v6c0 5 3.5 7.5 8 9 4.5-1.5 8-4 8-9V6l-8-3Z" />
          <path d="m9 12 2.2 2.2L15.5 10" />
        </svg>
      );
    case "layers":
      return (
        <svg {...common} className="size-6">
          <path d="m12 3 9 5-9 5-9-5 9-5Z" />
          <path d="m3 13 9 5 9-5" />
          <path d="m3 8 9 5 9-5" />
        </svg>
      );
    case "chart":
      return (
        <svg {...common} className="size-6">
          <path d="M4 20V10M12 20V4M20 20v-7" />
        </svg>
      );
    case "globe":
      return (
        <svg {...common} className="size-6">
          <circle cx="12" cy="12" r="9" />
          <path d="M3 12h18M12 3c2.5 2.6 4 6 4 9s-1.5 6.4-4 9c-2.5-2.6-4-6-4-9s1.5-6.4 4-9Z" />
        </svg>
      );
    case "clock":
      return (
        <svg {...common} className="size-6">
          <circle cx="12" cy="12" r="9" />
          <path d="M12 7v5l3.5 2" />
        </svg>
      );
  }
}

const colsClass: Record<2 | 3 | 4, string> = {
  2: "sm:grid-cols-2",
  3: "sm:grid-cols-2 lg:grid-cols-3",
  4: "sm:grid-cols-2 lg:grid-cols-4",
};

function FeatureCard({ f, i }: { f: IconFeature; i: number }) {
  const reduce = useReducedMotionSafe();
  return (
    <motion.div
      initial={reduce ? false : { opacity: 0, y: 16 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.4 }}
      transition={{ type: "spring", stiffness: 280, damping: 24, delay: i * 0.06 }}
      whileHover={reduce ? undefined : { rotateX: -4, rotateY: 4, y: -2 }}
      style={{ transformPerspective: 800 }}
      className="group rounded-3xl border border-border bg-card p-6 transition-colors hover:border-foreground/50"
    >
      <span className="inline-flex size-11 items-center justify-center rounded-xl bg-signal/10 text-signal transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:scale-105">
        {f.icon && <Icon name={f.icon} />}
      </span>
      <h3 className="mt-4 font-display text-lg font-bold tracking-tight text-foreground">{f.title}</h3>
      <p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{f.description}</p>
    </motion.div>
  );
}

export function IconFeatureGrid({ eyebrow = "Why Loopkit", title = "Everything an agent app needs, nothing it doesn't", features = DEFAULT_FEATURES, columns = 3, className }: IconFeatureGridProps) {
  const cols = (Number(columns) as 2 | 3 | 4) in colsClass ? (Number(columns) as 2 | 3 | 4) : 3;
  return (
    <section className={cn("w-full bg-background px-4 py-20 sm:px-6 lg:px-10", className)}>
      <div className="mx-auto max-w-3xl text-center">
        <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">{eyebrow}</p>
        <h2 className="mt-3 font-display text-[clamp(2rem,5vw,3.5rem)] font-extrabold leading-[0.95] tracking-[-0.03em] text-foreground">{title}</h2>
      </div>
      <div className={cn("mx-auto mt-12 grid max-w-6xl grid-cols-1 gap-4", colsClass[cols])}>
        {features.map((f, i) => (
          <FeatureCard key={f.title} f={f} i={i} />
        ))}
      </div>
    </section>
  );
}

export default IconFeatureGrid;

How it behaves

  • →Cards spring into view on scroll with a per-card stagger.
  • →On hover, each card tilts slightly in 3D toward the cursor direction and the icon badge lifts; disabled under prefers-reduced-motion.
  • →columns switches the grid between 2, 3 and 4 columns at the lg breakpoint without changing markup.
  • →Six built-in line icons (bolt, shield, layers, chart, globe, clock) are drawn inline as SVG, so no icon font or image request is needed.

Accessibility

  • ✓Icons are decorative and aria-hidden; the title and description carry the meaning.
  • ✓Tilt and lift effects are skipped under prefers-reduced-motion.
  • ✓Cards keep sufficient contrast between the icon badge tint and both themes' background.
  • ✓Honours prefers-reduced-motion.