Skip to content

STO Staggered Card Reveals

The hero section enters as a single unit — a heading, subheading, and button in tight sequence. The tour cards present a different challenge: multiple sibling elements that should each animate in turn, without you writing a separate animation block for every one of them.

This lesson applies slideUpFade to the .tour-card elements in .tours-grid using :nth-child() to give each card a progressively longer delay. The result is a cascade where cards appear to drop into place one after another as the grid loads.

Applying the same animation to every card in the grid is one line:

.tours-grid .tour-card {
animation: slideUpFade 500ms ease-out both;
}

Every card animates — but they all start at the same instant and arrive together. That is not a stagger; it is a synchronized fade. To create a cascade, each card needs a later delay than the one before it.

You could add a class to each card (card-1, card-2, card-3) and write a separate animation rule per class. That works, but it ties your CSS to your HTML count. Add a fourth card and you need a fourth CSS rule. :nth-child() handles this without touching the HTML.

:nth-child(n) targets an element based on its position among its siblings. Apply the base animation to all cards, then override animation-delay on subsequent siblings:

.tours-grid .tour-card {
animation: slideUpFade 500ms ease-out both;
}
.tours-grid .tour-card:nth-child(2) { animation-delay: 100ms; }
.tours-grid .tour-card:nth-child(3) { animation-delay: 200ms; }
.tours-grid .tour-card:nth-child(4) { animation-delay: 300ms; }
.tours-grid .tour-card:nth-child(5) { animation-delay: 400ms; }
.tours-grid .tour-card:nth-child(6) { animation-delay: 500ms; }

The base rule covers the first card with an implicit 0ms delay. Each subsequent :nth-child() rule only overrides animation-delay — the name, duration, timing function, and fill-mode all inherit from the base rule.

100ms per card is a tight stagger — all six cards complete within about one second of the page loading. This is appropriate for a grid: the visual pattern is clear without the entrance dragging on. For a hero sequence with larger, more prominent elements, larger gaps (150ms+) feel more deliberate. For a dense grid of many small items, 50–75ms keeps the stagger visible but fast.

How this interacts with the existing hover transition

Section titled “How this interacts with the existing hover transition”

The tour cards already have a hover lift and image zoom from CSS Module 08:

.tour-card {
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}
.tour-card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-lg);
}

transition and animation target the same transform property here, but they do not conflict because they operate at different times:

  • During the entrance animation, @keyframes controls transform (the browser ignores the transition while an animation is actively running on that property).
  • After the animation ends, the animation has no further claim on transform. The hover transition takes over cleanly when the user mouses over the card.

The one edge case: if a user hovers a card while its entrance animation is still running (within the first 500ms of page load), the hover state may not apply until the animation finishes. This is an acceptable tradeoff — a card mid-entrance is not expected to respond to hover immediately.

CSS staggers vs. scroll-triggered animation

Section titled “CSS staggers vs. scroll-triggered animation”

Applying animation-fill-mode: both to all cards keeps them invisible during their delay periods. On the homepage featured tours section, all three cards are typically visible in the initial viewport — the stagger fires immediately on load and the user watches each card arrive.

On the tours page, cards further down the grid may be below the initial viewport. Those cards are invisible during their delay, finish their animation, and stay visible (held by fill-mode: forwards) — which means a user who scrolls slowly may catch a card mid-animation, and a user who loads the page and immediately scrolls to the bottom will find those cards already fully visible.

This is a CSS-only approximation of a scroll-triggered reveal. It looks like scroll triggering when the user reads at a normal pace; it breaks down when the user scrolls immediately.

True scroll-triggered animation — where each card’s entrance waits until it enters the viewport, regardless of when the page loaded — requires the Intersection Observer API. You will implement that pattern in JavaScript Foundations. The CSS stagger you are building here is the foundation: the @keyframes and animation declarations remain unchanged; the only difference in the JavaScript version is that the animation class is added at the moment the element scrolls into view rather than at page load.

No HTML changes are needed. The .tours-grid and .tour-card class names are already in both index.html (featured tours) and tours.html. Add the animation rules to style.css below your existing hero entrance animations:

/* STO Tour Card Stagger */
.tours-grid .tour-card {
animation: slideUpFade 500ms ease-out both;
}
.tours-grid .tour-card:nth-child(2) { animation-delay: 100ms; }
.tours-grid .tour-card:nth-child(3) { animation-delay: 200ms; }
.tours-grid .tour-card:nth-child(4) { animation-delay: 300ms; }
.tours-grid .tour-card:nth-child(5) { animation-delay: 400ms; }
.tours-grid .tour-card:nth-child(6) { animation-delay: 500ms; }

The .tours-grid .tour-card selector scopes the animation to cards inside a tours grid specifically — it will not accidentally apply to other cards elsewhere on the site.

  1. Add the tour card stagger rules above to style.css.

  2. Open index.html (the homepage) in the browser. Scroll down to the featured tours section and reload the page. You should see the three featured tour cards animate in with a visible cascade — each arriving slightly after the one before it.

  3. Open tours.html. The same rules apply to the tours listing grid. Reload and watch the cards arrive in sequence. If you have six cards, the sixth card’s delay is 500ms — it begins animating half a second after page load.

  4. Open DevTools Animations panel and slow playback to 25%. Confirm each card starts at a distinct point on the timeline and that the stagger interval is consistent.

  5. Try changing the stagger interval from 100ms to 60ms and then to 150ms. Reload and observe how each feels. The difference between “crisp cascade” and “slow sequence” is in that single value.

  6. Confirm that hovering a card after the animation completes still lifts it correctly. The transition and animation should not interfere once the entrance is done.

  • Apply the base animation once to .tours-grid .tour-card, then override only animation-delay on each :nth-child() selector. This pattern scales to any card count without changing the animation name, duration, or fill-mode.
  • 100ms per card is a reliable stagger interval for grids. Larger elements or featured sequences warrant 150ms+. Dense grids of small items can go as low as 50ms.
  • transition and animation on the same property do not conflict — the animation controls the property while it runs; the transition takes over afterward for state changes like hover.
  • CSS page-load staggers are not true scroll-triggered animation. Cards below the fold animate on page load and are already in their final state by the time the user scrolls to them. True scroll-triggered reveals — waiting for each card to enter the viewport — require JavaScript (covered in JavaScript Foundations).

Lesson 07 addresses prefers-reduced-motion — the media query that lets you respect users who have opted out of motion effects in their operating system. Wrapping your animations in this query is the last step before the module is production-ready.