Next.js Features Section: 3-Column Cards and Stagger Animation

Published: September 12, 2026 · 5 min read

Build a Next.js features section: headings people read, Tailwind 3-column cards, a Framer Motion staggered entrance, subtle hover, and alternating rows.

Next.js features section: three or six “what you can do” cards

You are building a landing page in Next.js and put a features section under the hero. Icons plus “Fast,” “Secure,” “Integrations” in three columns, and suddenly it looks like every other SaaS page. This guide is for that.

A features section gets read when each card heading says what the visitor can now do, not what the feature is called. Use three or six cards. For motion, cards rising slightly and settling one after another is enough.

What you will get from this article:

  • How to write feature headings and copy, with before/after examples

  • A 3-column features section in Tailwind CSS

  • A staggered card entrance with Motion (formerly Framer Motion)

  • A quiet hover that changes only the border and icon

  • An alternating screenshot layout

Why features sections get skimmed

The features section is where people slow down and read. The hero got their interest; now they want to know what it actually does. Skimming usually comes from one of three things.

  • Headings are just feature names, and the body restates them (“Fast” followed by “It runs fast”)

  • Every icon has the same shape and weight, so removing them changes nothing

  • The cards use hero-strength motion, so eyes follow the movement instead of the words

These look like visual problems, but the root cause is copy that never says what changes for the visitor. Fix the card text before touching the design.

Rewriting features section headings: examples

Replace the feature name with how the visitor's work changes. In the body, add who, what, and how much in one or two sentences.

  • “Fast” → “Pages load before visitors give up”

  • “Security” → “Customer data never leaves your environment”

  • “Integrations” → “Inquiries land straight in Slack”

  • “Analytics” → “See which ad brought each signup”

Use three or six feature cards

In a 3-column grid, three or six cards leave no gaps. Four or five leave a hole in the last row and look unfinished.

Nine or more reads as “we listed everything,” and nothing gets read. If you have more to say, keep the top six and move the rest to pricing or the FAQ.

Build a 3-column features section with Tailwind CSS

Start with a static version. Each card has an icon, a heading, and about two lines of body text. Keeping the data in an array means copy changes happen in one place.

This component holds no state, so it stays a Server Component. Icons come from lucide-react, which shadcn/ui projects often use (npm i lucide-react).

components/Features.tsx
import { Clock, ShieldCheck, Zap } from "lucide-react";

  const features = [
    {
      icon: Zap,
      title: "Pages load before visitors give up",
      body: "Images are optimized automatically, so the first screen appears fast even on mobile data.",
    },
    {
      icon: ShieldCheck,
      title: "Customer data never leaves your environment",
      body: "Data is stored in your own infrastructure, with permissions per teammate.",
    },
    {
      icon: Clock,
      title: "Go from draft to live in one day",
      body: "Paste in your copy and get a preview URL right away.",
    },
  ];

  export function Features() {
    return (
      <section id="features" className="mx-auto max-w-6xl px-6 py-24">
        <p className="text-sm font-medium text-indigo-600">Features</p>
        <h2 className="mt-2 text-3xl font-semibold tracking-tight md:text-4xl">
          Spend less time building, more time shipping
        </h2>
        <p className="mt-4 max-w-2xl text-zinc-600">
          Three features that cut the work between idea and launch.
        </p>

        <ul className="mt-12 grid gap-6 md:grid-cols-3">
          {features.map(({ icon: Icon, title, body }) => (
            <li key={title} className="rounded-2xl border border-zinc-200 p-6">
              <Icon aria-hidden className="h-6 w-6 text-indigo-600" />
              <h3 className="mt-4 font-semibold">{title}</h3>
              <p className="mt-2 text-sm leading-relaxed text-zinc-600">{body}</p>
            </li>
          ))}
        </ul>
      </section>
    );
  }

What the section heading and lead do

Above the cards, place a small label, an h2, and a one-line lead. The h2 says what changes across all the cards, not a list of features. Card headings are h3, so the heading order never skips a level.

Consistent spacing looks finished without motion

Matching card padding, grid gap, and section padding changes the impression more than you would expect. A good baseline: 24px inside cards, 24px between cards, 96px above and below the section. Add motion only once this looks right.

Stagger animation for features section cards

When the section scrolls into view, cards rise about 16px and settle 0.08 seconds apart. That is all the motion a features section needs. Motion's variants (named animation states) with stagger (a per-child delay set on the parent) let the parent control each card's delay.

Split only the animated parts into a small component that runs in the browser, marked with "use client" at the top of the file. The features section itself stays on the server; swap its list and list items for these wrappers.

components/Reveal.tsx
"use client";

  import { motion, stagger, type Variants } from "motion/react";
  import type { ReactNode } from "react";

  const list: Variants = {
    hidden: {},
    show: { transition: { delayChildren: stagger(0.08) } },
  };

  const item: Variants = {
    hidden: { opacity: 0, y: 16 },
    show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
  };

  export function RevealList({ children, className }: { children: ReactNode; className?: string }) {
    return (
      <motion.ul
        className={className}
        variants={list}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true, amount: 0.3 }}
      >
        {children}
      </motion.ul>
    );
  }

  export function RevealItem({ children, className }: { children: ReactNode; className?: string }) {
    return (
      <motion.li className={className} variants={item}>
        {children}
      </motion.li>
    );
  }

Swap the features list for the entrance wrappers

Replace the list (ul) with RevealList and each card (li) with RevealItem. Your existing classes pass through unchanged.

components/Features.tsx (excerpt)
<RevealList className="mt-12 grid gap-6 md:grid-cols-3">
    {features.map(({ icon: Icon, title, body }) => (
      <RevealItem key={title} className="rounded-2xl border border-zinc-200 p-6">
        <Icon aria-hidden className="h-6 w-6 text-indigo-600" />
        <h3 className="mt-4 font-semibold">{title}</h3>
        <p className="mt-2 text-sm leading-relaxed text-zinc-600">{body}</p>
      </RevealItem>
    ))}
  </RevealList>

Play the card animation once, not on every scroll

With viewport once set to true, cards that have appeared do not animate again when you scroll back. Cards vanishing and reappearing every time you reread them gets in the way. amount: 0.3 means play once 30% of the section is visible.

Feature card hover: change the border and icon

A big hover shadow makes a card look like a button. Feature cards are usually not links, so keep the change small: darken the border slightly and lift the icon by 2px.

components/Features.tsx (card classes)
<RevealItem className="group rounded-2xl border border-zinc-200 p-6 transition-colors hover:border-indigo-300">
    <Icon
      aria-hidden
      className="h-6 w-6 text-indigo-600 transition-transform duration-300 group-hover:-translate-y-0.5 motion-reduce:transition-none"
    />
    {/* heading and body unchanged */}
  </RevealItem>

Alternate rows when a feature is a screen

When the feature is the UI itself (a dashboard, an editor), a large screenshot explains more than a card. Put text and image side by side, and flip every other row.

Use next/image with width and height to prevent layout shift. Keep entrance motion weaker than the hero's. For the hero, see https://design-layer.com/en/articles/nextjs-animated-hero-section-guide

components/FeatureRows.tsx
import Image from "next/image";

  const rows = [
    {
      title: "Follow every signup on one screen",
      body: "See which ad they came from and where they dropped off, in order.",
      image: "/features/dashboard.png",
      alt: "Dashboard showing the signup funnel",
    },
    {
      title: "Edit copy right in the browser",
      body: "Change the live page, review it, then publish.",
      image: "/features/editor.png",
      alt: "Editing page copy in the browser",
    },
  ];

  export function FeatureRows() {
    return (
      <section className="mx-auto max-w-6xl space-y-24 px-6 py-24">
        {rows.map((row, i) => (
          <div
            key={row.title}
            className={`flex flex-col items-center gap-10 md:flex-row ${i % 2 === 1 ? "md:flex-row-reverse" : ""}`}
          >
            <div className="md:w-1/2">
              <h3 className="text-2xl font-semibold">{row.title}</h3>
              <p className="mt-4 leading-relaxed text-zinc-600">{row.body}</p>
            </div>
            <Image
              src={row.image}
              alt={row.alt}
              width={1200}
              height={800}
              className="rounded-2xl border border-zinc-200 md:w-1/2"
            />
          </div>
        ))}
      </section>
    );
  }

Features section cards vs. a bento grid

Bento grids, with mixed tile sizes, are common on landing pages now. Choose based on whether you are showing text or visuals.

  • Mostly explanatory text: 3-column cards read better

  • Photos, numbers, or small demos you want large: a bento grid fits

  • The product screen itself: alternating rows explain it best

For bento grids, see https://design-layer.com/en/articles/nextjs-bento-grid-guide

Copy a finished features section instead

The Scroll Feature Showcase page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right
The Scroll Feature Showcase page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right

Cards that lift on hover, icons that move slightly, a tuned entrance stagger: building that block from scratch means hours of tweaking ratios and spacing.

DesignLayer is a catalog of human-built React + Tailwind motion sections you can live-preview in the browser, then copy. Browsing needs no account and no card. https://design-layer.com/en/components

A free account copies the 10 Free Sample components with no monthly limit on a free account, plus 1 video prompt a month. Free cannot connect MCP. The rest of the catalog is the 30-Day Build Pass ($19 once, no auto-renewal), Lifetime Access ($79 once, first 100 buyers), or Plus ($9/month). Taking features and pricing on the same day pulls the middle of your page together. Pricing guide: https://design-layer.com/en/articles/nextjs-pricing-table-guide

Next.js features section: common questions

  • In a 3-column grid, three or six. No gaps in the last row, and people read to the end. Move anything beyond that to pricing or the FAQ.

Summary: building a features section in Next.js

  • Write card headings as what the visitor can do, not feature names

  • Use three or six cards; move extras to pricing or the FAQ

  • Keep motion to a light stagger and play it only once

  • On hover, change border and icon color, not the shadow

  • Alternating rows for screens, a bento grid for large photos or numbers

Start by rewriting your current feature headings using the examples above. For more layout options, read https://design-layer.com/en/articles/nextjs-bento-grid-guide and for scroll-triggered motion, https://design-layer.com/en/articles/nextjs-scroll-reveal-guide

Related guides