Stacked cards fan out horizontally on scroll.
- ID:
card-spread - Target shape: Best for 3–7 similarly sized sibling items inside a single sticky stage, including mixed-content wrappers where the cards can share one overlapped grid row.
- Description: Five cards stacked at the center of the viewport fan out left/right and shrink slightly as the section scrolls past.
<section class="scroll-section">
<div class="cards-container-wrapper">
<div id="cards-collection">
<h2 class="static-title">Title</h2>
<div id="card-1" class="card">1</div>
<div id="card-2" class="card">2</div>
<div id="card-3" class="card">3</div>
<div id="card-4" class="card">4</div>
<div id="card-5" class="card">5</div>
</div>
</div>
</section>- Role ownership is strict:
scrollSectionowns the timeline,stickyStageowns sticky/clipping,collectionowns the centered inner stage, andrepeatedCardowns the overlapped card-stage layout plus spread transform. stickyStageandcollectionmust be different selectors. In Wix,stickyStageis the internal-container-root#comp-...withdata-testid="internal-container-root"andcollectionis its[data-testid="internal-container-content"]child.- If
collectionalso contains non-repeated siblings such as titles or copy, keepcollectionas a grid and overlap only the repeated cards in a shared card stage row. Do not convert the whole mixed wrapper to flex. - Keep card-spread layout styles on the repeated card roots, not on raw
imgdescendants or broad selectors when concrete card component ids exist. - Repeated cards share one overlapped stage inside the collection, not sticky items. Use rendered
#comp-...ids, notDESKTOP--...ids.
| Role | Guidance |
|---|---|
scrollSource |
The tall section that drives the viewProgress trigger. |
stickyStage |
A sticky viewport-height wrapper that keeps the cards pinned during scroll. |
collection |
The grid layout owner that can keep static siblings in flow while repeated cards share one overlapped card stage. |
repeatedCard |
Repeated sibling items that share one overlapped grid cell and then spread horizontally. |
- Preserve the section root outer layout; the sticky stage and centered collection are inner roles, not section-root roles.
- Use viewport units only for the outer timeline container and sticky stage. Size cards relative to the collection stage so their proportions stay close to the source composition.
- If
collectioncontains a title or other static siblings, leave them in their own normal grid row and place only the repeated cards into a shared lower grid row so the non-animated content stays untouched. - Animate spread with
translateX(...) scale(...)on the card roots instead of resizing card height unless the real section truly depends on viewport-sized cards. - When item count changes, recompute width, spacing, and outer translation distances instead of copying the demo offsets literally.
- If the outer cards land mostly outside the visible stage, reduce card width or spread distance before returning the result.
| Key | Role | Demo Selector | Purpose |
|---|---|---|---|
scrollSection |
scrollSource |
.scroll-section |
The viewProgress source for the entire pattern. |
stickyStage |
stickyStage |
.cards-container-wrapper |
Sticky pin only: position: sticky, 100vh, overflow: clip. Wix: #comp-... with data-testid="internal-container-root" — not the collection. |
collection |
collection |
#cards-collection |
Centered mixed-content stage for the spread. Wix: #<stickyStageCompId> [data-testid="internal-container-content"] — must differ from stickyStage. |
card1 |
repeatedCard |
.scroll-section #card-1 |
Minimum repeated spread card; extend outward for card4..cardN. |
card2 |
repeatedCard |
.scroll-section #card-2 |
Repeated spread card. |
card3 |
repeatedCard |
.scroll-section #card-3 |
Repeated spread card. |
card4 |
repeatedCard |
.scroll-section #card-4 |
Repeated spread card. |
card5 |
repeatedCard |
.scroll-section #card-5 |
Repeated spread card. |
Repeated card keys must keep their trailing index (
card1,card2, …) so they compact into thecard{n}group; extend the row ascard4..cardNfor more items.
.scroll-section {
height: 400vh;
}Reason: creates enough scroll distance for the full viewProgress spread to play out.
.cards-container-wrapper {
position: sticky;
top: 0;
height: 100vh;
overflow: clip;
}Reason: pins the stage to the viewport and clips the spreading cards while the source section scrolls.
#cards-collection {
position: relative;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
width: 100%;
height: 100vh;
margin: 0 auto;
justify-items: center;
}Reason: creates a mixed-content grid stage so static siblings stay in flow while repeated cards overlap in a shared card row. The collection owns the composition space; child card percentages resolve against this stage.
#cards-collection > .card {
grid-column: 1;
grid-row: 2;
place-self: start center;
width: 20vw;
height: 55%;
transform-origin: center center;
will-change: transform;
}Reason: overlaps repeated cards in one shared grid cell with top alignment and centered placement before the animation distributes them, preserving their proportion relative to the collection stage.
.card {
margin: 0;
}Reason: prevents repeated cards from drifting apart because of default spacing.
Always expose at least the spread distance and ending scale; add more only when the adapted experience introduces new stable knobs.
- Label:
Spread - Group:
Layout - Type:
range - Default:
40 - Description: Controls how far the outer cards fan out from the center stage at the end of the scroll range.
- Constraints:
min: 20,max: 50,step: 2,unit: vw - Suggested variable:
--card-spread-unit
- Label:
Card Scale - Group:
Layout - Type:
range - Default:
0.85 - Description: Controls the ending scale of the cards at maximum spread.
- Constraints:
min: 0.7,max: 1,step: 0.01,unit: x - Suggested variable:
--card-end-scale
const RANGE = {
rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 20 } },
rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 80 } },
easing: 'cubic-bezier(0.42, 0, 0.58, 1)',
fill: 'both' as const,
};
// Demo offsets for five cards — recompute from real item count and spread.
const SPREAD_TRANSLATIONS = ['-40vw', '-20vw', '0', '20vw', '40vw'] as const;
// Combined per-card effect: translateX + scale shrink in a single keyframe pair.
const cardSpreadEffect = (key: string, endTranslate: string) => ({
key,
keyframeEffect: {
name: `${key}-spread`,
keyframes: [
{ transform: 'translateX(0) scale(1)' },
{ transform: `${endTranslate} scale(0.85)` },
],
},
...RANGE,
});
const interaction = {
key: 'scrollSection',
trigger: 'viewProgress',
effects: SPREAD_TRANSLATIONS.map((translate, index) =>
cardSpreadEffect(`card${index + 1}`, `translateX(${translate})`),
),
};