Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
ainergiz avatar

Stacked Cards

  • 7 installs
  • 6 repo stars
  • Updated February 8, 2026
  • ainergiz/design-inspirations

stacked-cards is a Claude Code skill that builds horizontally fanned card stacks with correct z-index ordering and hover-lift animations.

About

stacked-cards is a Claude Code skill that builds horizontally fanned or cascading card stacks. It solves wrong stacking order, hover breaking the cascade, and tooltips trapped in a stacking context. A developer uses it when building album browsers, card fans, or overlapping card collections.

  • Reverse render order so the first card appears on top with the highest z-index
  • Hover lifts a card without changing z-index so the cascade stays intact
  • Renders the tooltip outside the stack to escape its stacking context

Stacked Cards by the numbers

  • 7 all-time installs (skills.sh)
  • Ranked #1,756 of 2,244 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Jul 28, 2026 (Skillselion catalog sync)
At a glance

stacked-cards capabilities & compatibility

Capabilities
stacked cards · z index stacking · hover animation
Use cases
frontend · ui design
Pricing
Free
From the docs

What stacked-cards says it does

Creates horizontally fanned/cascading card stacks with proper z-index ordering and hover lift animations.
SKILL.md
Hover lifts card UP without changing z-index
SKILL.md
npx skills add https://github.com/ainergiz/design-inspirations --skill stacked-cards

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs7
repo stars6
Last updatedFebruary 8, 2026
Repositoryainergiz/design-inspirations

What it does

Building horizontally fanned card stacks with correct z-index ordering and hover-lift animations.

Who is it for?

Album browsers, card fans, and overlapping card collections needing correct stacking order.

Skip if: Simple grids or non-overlapping card lists.

When should I use this skill?

When building album browsers, card fans, stacked previews, or any overlapping card collection.

What you get

A card stack with correct front-to-back order, in-place hover lift, and escaping tooltips.

  • StackedCards component with z-index ordering, hover lift, and escaping tooltip

By the numbers

  • Solves 3 common stacking bugs
  • 40px horizontal offset between cards

Files

SKILL.mdMarkdownGitHub ↗

Stacked Cards Pattern

Build horizontally cascading card stacks where cards overlap in order, with hover animations that lift cards in place without breaking the cascade.

Why This Pattern?

Stacked cards have three common bugs:

1. Wrong stacking order - Later cards in the array appear on top 2. Hover breaks cascade - Changing z-index on hover disrupts the visual order 3. Tooltip trapped in stacking context - Tooltips inside cards can't escape their parent's z-index

This pattern solves all three.

Core Concept

First card (front)    Last card (back)
     ↓                      ↓
┌─────┐
│     │┌─────┐
│  1  ││     │┌─────┐
│     ││  2  ││     │
└─────┘│     ││  3  │
       └─────┘│     │
              └─────┘
  • First item in array = front (highest z-index)
  • Each subsequent item = behind and offset right
  • Hover lifts card UP without changing z-index

Core Implementation

"use client";

import { useState } from "react";
import Image from "next/image";

interface Card {
  id: string;
  imageUrl: string;
  title: string;
}

function StackedCards({ cards }: { cards: Card[] }) {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);

  const spacing = 40;      // Horizontal offset between cards
  const cardSize = 130;    // Card width
  const stackWidth = (cards.length - 1) * spacing + cardSize;

  return (
    <div className="relative h-[180px] w-full flex items-center justify-center">
      {/* CRITICAL: Reverse render order so first card renders LAST in DOM (appears on top) */}
      {[...cards].reverse().map((card, renderIndex, arr) => {
        // Convert render index back to actual card index
        const cardIndex = arr.length - 1 - renderIndex;

        // Center the stack horizontally
        const translateX = -stackWidth / 2 + cardIndex * spacing + cardSize / 2;

        // Z-index: first card (index 0) has HIGHEST z-index
        const zIndex = arr.length - cardIndex;

        // Hover: lift UP only, do NOT change z-index
        const translateY = hoveredIndex === cardIndex ? -20 : 0;

        return (
          <div
            key={card.id}
            className="absolute left-1/2 cursor-pointer transition-all duration-300 ease-out"
            style={{
              transform: `translateX(calc(-50% + ${translateX}px)) translateY(${translateY}px)`,
              zIndex,
            }}
            onMouseEnter={() => setHoveredIndex(cardIndex)}
            onMouseLeave={() => setHoveredIndex(null)}
          >
            <div className="relative w-[130px] h-[130px] rounded-2xl overflow-hidden shadow-xl">
              <Image
                src={card.imageUrl}
                alt={card.title}
                fill
                className="object-cover"
              />
            </div>
          </div>
        );
      })}

      {/* Tooltip rendered OUTSIDE the stack to escape z-index stacking context */}
      {hoveredIndex !== null && cards[hoveredIndex] && (() => {
        const translateX = -stackWidth / 2 + hoveredIndex * spacing + cardSize / 2;
        const card = cards[hoveredIndex];

        // Position: center (50%) - half card - lift - gap
        const tooltipTop = 'calc(50% - 95px)';

        return (
          <div
            className="absolute px-3 py-2 rounded-xl whitespace-nowrap shadow-lg z-50 pointer-events-none bg-white/95 backdrop-blur-md border border-zinc-200/80"
            style={{
              left: '50%',
              top: tooltipTop,
              transform: `translateX(calc(-50% + ${translateX}px)) translateY(-100%)`,
            }}
          >
            <p className="text-sm font-semibold text-zinc-900">{card.title}</p>
          </div>
        );
      })()}
    </div>
  );
}

Key Elements

1. Reverse Render Order

// CORRECT - First card renders LAST in DOM, appears on top
{[...cards].reverse().map((card, renderIndex, arr) => {
  const cardIndex = arr.length - 1 - renderIndex;
  // ...
})}

// WRONG - First card renders first, appears BEHIND others
{cards.map((card, index) => {
  // ...
})}

Why? DOM order determines stacking when z-index values are the same within a parent. By rendering in reverse, the first logical card is the last DOM element, appearing on top.

2. Z-Index Without Hover Change

// CORRECT - Z-index based only on position, unchanged on hover
const zIndex = arr.length - cardIndex;

// WRONG - Changing z-index on hover breaks the cascade
const zIndex = hoveredIndex === cardIndex ? 20 : arr.length - cardIndex;

Why? When you change z-index on hover, the card jumps to the front, breaking the visual illusion of a physical stack. Real cards lift UP in place while staying behind cards in front.

3. Hover Lift Only (No Scale)

// CORRECT - Only translateY, preserves cascade illusion
const translateY = hoveredIndex === cardIndex ? -20 : 0;

// AVOID - Scale makes card "pop out" visually
const scale = hoveredIndex === cardIndex ? 1.05 : 1;

4. Tooltip Outside Stacking Context

// WRONG - Tooltip inside card div is trapped by parent's z-index
<div style={{ zIndex: 3 }}>
  <div className="card">...</div>
  {hovered && <div className="tooltip z-50">...</div>}  {/* z-50 doesn't help! */}
</div>

// CORRECT - Tooltip as sibling, outside all card divs
{cards.map(...)}
{hoveredIndex !== null && (
  <div className="absolute z-50" style={{ /* calculated position */ }}>
    Tooltip content
  </div>
)}

Why? A child element cannot escape its parent's stacking context. Tooltips inside cards with lower z-index will be covered by sibling cards with higher z-index.

5. Centering Formula

const spacing = 40;      // Gap between card left edges
const cardSize = 130;    // Card width
const stackWidth = (cards.length - 1) * spacing + cardSize;

// For each card, calculate horizontal offset from center
const translateX = -stackWidth / 2 + cardIndex * spacing + cardSize / 2;

Then use left-1/2 with translateX(calc(-50% + ${translateX}px)) for centering.

6. Clear Hover State on Navigation

const handleSelectCard = (index: number) => {
  setHoveredIndex(null);  // Clear before view change
  setSelectedCard(index);
  setView("detail");
};

const handleBack = () => {
  setHoveredIndex(null);  // Clear when returning
  setView("list");
};

Why? Without clearing, the previously hovered card stays elevated when returning to the list view.

Tooltip Position Calculation

Position the tooltip above the lifted card:

// Container height: 180px, card height: 130px, lift: 20px
// Card top when lifted = center - halfCard - lift = 90 - 65 - 20 = 5px

// Tooltip should be above this with gap
// Position: center (50%) - halfCard (65px) - lift (20px) - gap (10px) = 50% - 95px
const tooltipTop = 'calc(50% - 95px)';

// translateY(-100%) moves tooltip up by its own height
style={{
  top: tooltipTop,
  transform: `translateX(...) translateY(-100%)`,
}}

Sizing Variations

ContextCard SizeSpacingLiftContainer Height
Preview80px28px-12px110px
Full130px40px-20px180px

Light/Dark Variants

ElementLight ModeDark Mode
Card shadowshadow-zinc-400/50shadow-black/70
Tooltip bgbg-white/95bg-zinc-800/95
Tooltip borderborder-zinc-200/80border-zinc-700/80
Tooltip texttext-zinc-900text-zinc-100

Checklist

  • [ ] Render order reversed with [...array].reverse().map()
  • [ ] Card index calculated from render index: arr.length - 1 - renderIndex
  • [ ] Z-index decreases with card index: arr.length - cardIndex
  • [ ] Z-index does NOT change on hover
  • [ ] Hover only applies translateY, no scale
  • [ ] Tooltip rendered OUTSIDE the card loop as sibling
  • [ ] Tooltip has pointer-events-none to avoid hover interference
  • [ ] Hover state cleared on view transitions
  • [ ] Container has relative with fixed height
  • [ ] Cards use absolute left-1/2 with calculated translateX

Related skills

FAQ

How does stacked-cards keep the right order?

It reverses the render order so the first card renders last in the DOM and gets the highest z-index.

Why render the tooltip outside the stack?

So the tooltip escapes the card's z-index stacking context and is not trapped behind other cards.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.