# Stacked Cards Widget

> Scroll-driven stack of cards that pin and reveal as the user scrolls. Powered by GSAP ScrollTrigger. Common pattern: feature comparisons that "stack" on scroll.

**Class file:** `includes/Elements/Stacked_Cards.php` (1,788 lines)
**Slug:** `stacked-cards` (widget id `eael-stacked-cards`)
**Public docs:** <https://essential-addons.com/elementor/docs/stacked-cards/>
**Pro-shared:** Pro-only. Vendor: GSAP + ScrollTrigger (Pro-bundled).

## Overview

Cards (Repeater rows) stack vertically and animate based on scroll position via GSAP ScrollTrigger. Each card pins as it enters viewport, then the next card scrolls atop it. Effect mimics "Apple-style" feature reveal. GSAP is the heaviest animation engine in EA Pro's bundle — shared with `smooth-animation` extension, `section-parallax`, `custom-cursor` (some effects), `interactive-cards`.

## Pro vs Lite

Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Stacked_Cards.php` | Widget class (1,788 lines) |
| `src/css/view/stacked-cards.scss` → `assets/front-end/css/view/stacked-cards.min.css` | Styling |
| `src/js/view/stacked-cards.js` → `assets/front-end/js/view/stacked-cards.min.js` | GSAP ScrollTrigger init |
| `assets/front-end/js/lib-view/gsap/gsap.min.js` | GSAP core (lib) |
| `assets/front-end/js/lib-view/gsap/ScrollTrigger.min.js` | ScrollTrigger plugin (lib) |
| `config.php` entry `'stacked-cards'` | GSAP + ScrollTrigger + Pro JS |

## Architecture

- **Composes Pro's `Helper`** (line 13) — uses Pro-specific helpers.
- **GSAP ScrollTrigger** for pin + reveal animation. Each card pinned via `ScrollTrigger.create({ trigger, pin: true, ... })`.
- **Sticky-pin offset** — configurable per card; user can tune how long each card stays pinned before next overtakes.
- **Repeater of cards** — each row: image / title / description / button.
- **Performance-sensitive** — ScrollTrigger uses `requestAnimationFrame`; mobile devices with low-end GPUs may stutter.

## Render Output

```html
<div class="eael-stacked-cards-wrap">
  <!-- One pinned card per Repeater row -->
  <div class="eael-stacked-card" data-card-index="0">
    <img src="{image}" />
    <h3>{title}</h3>
    <p>{description}</p>
    [?] <a class="eael-card-button">{button}</a>
  </div>
  <div class="eael-stacked-card" data-card-index="1">...</div>
  ...
</div>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| Cards Repeater | Content → Cards | REPEATER | Per-card data |
| Pin duration | Content → Animation | NUMBER | px / scroll-distance per pin |
| Stack offset | Content → Animation | SLIDER | Vertical offset between stacked cards |
| Easing | Content → Animation | SELECT | GSAP easing |
| Per-card styling | Style → ... | various | Heavy controls |

## Conditional Dependencies

Standard Repeater + per-card visibility toggles.

## JavaScript Lifecycle

```js
gsap.registerPlugin( ScrollTrigger );
$cards.each( function( index, card ) {
    ScrollTrigger.create( {
        trigger: card,
        start: 'top top+=' + ( index * stackOffset ),
        pin: true,
        pinSpacing: false,
        end: 'bottom top+=' + ( ( index + 1 ) * stackOffset ),
        scrub: 1
    } );
} );
```

## Hooks & Filters

Standard widget render.

## Common Issues

| Symptom | Cause | Fix |
| --- | --- | --- |
| Cards don't pin | ScrollTrigger not loaded | Verify GSAP + ScrollTrigger in config.php |
| Pin breaks under sticky header | `start` offset not accounting for sticky | Adjust `start: 'top top+={header-height}'` |
| Cards stack but no scroll-tied animation | `scrub` not set | Ensure `scrub: 1` or numeric for tied animation |
| Editor preview broken | ScrollTrigger needs DOM ready | Add `ScrollTrigger.refresh()` after editor preview reload |
| Mobile janky | Heavy GSAP + many cards | Reduce cards on mobile via responsive controls |

## Known Limitations

- **No `prefers-reduced-motion`** — pin/scrub runs regardless
- **GSAP + ScrollTrigger ~120KB** combined — heavy for widget that may only render a few cards
- **Pin breaks under sticky headers** without manual offset config
- **Editor preview**: ScrollTrigger requires `refresh()` after Elementor preview reload; verify wiring
- **1,788 lines** — per-card style controls dominate

## Cross-References

- Sibling: [`interactive-cards.md`](interactive-cards.md), [`counter.md`](counter.md)
- Extensions using GSAP: [`section-parallax`](../extensions/section-parallax.md), [`smooth-animation`](../extensions/smooth-animation.md), [`custom-cursor`](../extensions/custom-cursor.md)
- Shared patterns: [`_patterns.md`](_patterns.md)
