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.