Source Code

<section class="scu-hero" id="scuHero">
  <div class="scu-copy">
    <span class="scu-eyebrow">Field notes, issue 12</span>
    <h1 class="scu-h1">A quieter way<br>to build software</h1>
    <p class="scu-sub">No dashboards shouting at you. No red badges. Just the tools you need, exactly when you need them.</p>
    <a href="#" class="scu-cta">Read the story</a>
  </div>

  <button class="scu-cue" id="scuCue" aria-label="Scroll down">
    <span class="scu-cue-arrow">
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
    </span>
    <span class="scu-cue-label">Scroll</span>
  </button>
</section>
<section class="scu-next">
  <p>You actually scrolled. The cue above already noticed and faded out.</p>
</section>

Hero with Animated Scroll Cue — Free HTML CSS JS Snippet

Hero with Animated Scroll Cue · Heroes · Plain HTML, CSS & JS · Live preview

What's included

Features

Real scroll-position detection
Reads window.scrollY, not a fixed delay.
Passive scroll listener
Never blocks scroll performance.
Correct on load
evaluateScroll() runs once immediately, handling mid-scroll loads.
Clickable, not decorative
A real <button> that triggers a smooth scroll.
CSS bounce, JS visibility
Motion and interactivity concerns cleanly separated.
Configurable threshold
One constant controls when it disappears.
Accessible label
aria-label describes the control for screen readers.
Reappears on scroll-up
State reflects current position, not a one-time fade.

About this UI Snippet

Hero with Animated Scroll Cue — Fades Out on Real Scroll, Not a Timer

Screenshot of the Hero with Animated Scroll Cue snippet rendered live

Most "scroll cue" implementations are pure CSS: a bouncing arrow that loops forever regardless of what the visitor does. This snippet's cue bounces via CSS, but its visibility is driven by an actual scroll event listener reading window.scrollY — so it disappears the moment the visitor has genuinely started scrolling, and stays put if they haven't.

Real scroll detection, not a setTimeout guess

The temptation with a scroll cue is to hide it after a fixed delay ("fade out after 4 seconds") — but that fades it out for a visitor who's still reading the headline, and leaves it visible for one who scrolled instantly. This snippet instead attaches a passive: true scroll listener that checks window.scrollY against a small HIDE_THRESHOLD (40px) on every scroll event, so the cue's visibility is a direct function of how far the page has actually moved — not a clock.

Why `{ passive: true }` matters

Scroll listeners can block the browser's scroll-performance optimizations unless marked passive, since the browser otherwise has to wait to confirm the listener won't call preventDefault(). Since this listener only reads scrollY and toggles a class — it never calls preventDefault() — marking it passive is both correct and free performance.

The bounce is CSS, the visibility is JS

The up-down bounce itself is a simple infinite @keyframes animation — that part genuinely can run forever with no downside, since it's purely decorative motion on an element that's either shown or hidden. The two concerns are cleanly separated: CSS owns "how it moves while visible," JavaScript owns "whether it's visible at all," which keeps the animation itself simple while the interactive logic stays testable.

A cue that also functions as a control

The scroll cue is a real <button>, not a decorative <div> — clicking it calls scrollIntoView({ behavior: 'smooth' }) on the hero's next sibling section, so it's an affordance you can act on directly rather than only a hint to scroll manually. It also has an aria-label for screen reader users.

Correct on load, not just after the first scroll

evaluateScroll() runs once immediately on script load (not just inside the listener), so a page that loads already scrolled — for example via a same-page anchor link — shows the cue in the correct hidden/visible state from the first paint instead of only updating on the next scroll event.

Customizing it

Adjust HIDE_THRESHOLD for a cue that disappears sooner or later, swap the arrow icon for a mouse-wheel glyph, or fade with an opacity transition instead of the visibility toggle. Pair it with scroll reveal grid or scroll zoom hero for more scroll-driven hero effects.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of guessing whether a scroll cue is doing real work, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the visibility check reads window.scrollY inside a passive scroll listener rather than hiding the cue after a fixed setTimeout, and what could go wrong for a visitor who loads the page already scrolled if evaluateScroll() weren't also called once immediately on load. The same assistant can help you refine the threshold and timing — ask whether 40px is the right hide threshold for a very tall hero versus a short one, or whether the fade should use an opacity transition instead of the current visibility toggle for a smoother disappearance. It's also useful for extending the pattern: ask it to add IntersectionObserver-based detection as an alternative to the scroll listener, throttle the scroll handler with requestAnimationFrame for very high-frequency scroll events, or swap the chevron icon for an animated mouse-wheel glyph. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a full-viewport hero section in plain HTML, CSS, and vanilla JavaScript with a bouncing scroll-down indicator that reacts to real scroll position (no library, no CDN, no setTimeout-based hiding).

Requirements:
- A full-height hero with a headline, subheading, and CTA link, and a scroll-cue control (a real <button>, not a plain div) fixed near the bottom center containing a bouncing chevron-down icon and a small "Scroll" label, with the bounce done via a simple infinite CSS keyframes animation.
- Attach a scroll event listener to the window with { passive: true } that reads the live window.scrollY (or document.documentElement.scrollTop) on every scroll event and toggles a "hidden" class on the cue once the scroll position exceeds a small threshold constant (e.g. 40px) — the cue's visibility must be a direct function of actual scroll position, not a fixed delay timer.
- The hidden state should use a CSS transition on opacity/visibility so the cue fades out smoothly rather than disappearing instantly, and the cue should reappear if the user scrolls back up above the threshold.
- Call the same scroll-check function once immediately when the script runs (not only inside the event listener) so the correct initial state is set even if the page loads already scrolled (e.g. via a hash link).
- Make the cue itself functional: clicking it should scroll smoothly to the next section below the hero using scrollIntoView({ behavior: "smooth" }), and give the button an appropriate aria-label since it's icon-only.
- Add a second section below the hero so there's something to scroll to and verify the cue's fade behavior against.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThe hero renders full-height with the cue bouncing at the bottom.
  2. 2
    Scroll down slightlyPast the 40px threshold, the cue fades out — this is a real scroll listener, not a timer.
  3. 3
    Scroll back to the topThe cue reappears since evaluateScroll() re-checks scrollY on every scroll event.
  4. 4
    Click the cue itselfIt performs a real smooth scroll to the next section.
  5. 5
    Tune the thresholdChange HIDE_THRESHOLD to control how much scroll hides the cue.
  6. 6
    Swap the iconReplace the chevron SVG with a mouse-wheel icon if preferred.

Real-world uses

Common Use Cases

Editorial and long-form landing pages
Signal there's more content below the fold.
Portfolio and case-study heroes
Pair with portfolio hero.
Single-page storytelling sites
Guide visitors into a scroll-driven narrative.
Product launch pages
Combine with minimal hero.
Agency reels and showcases
Nudge visitors past a bold opening statement.
Documentation landing pages
Hint at content beneath a large intro section.
Related: Hero with 3D Tilting Product Mockup
See the Hero with 3D Tilting Product Mockup for a related heroes pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

On real scrolling. A scroll event listener reads window.scrollY on every scroll event and toggles a hidden class once it exceeds a small threshold (40px by default). There is no setTimeout involved — a visitor who never scrolls keeps seeing the cue indefinitely, and one who scrolls immediately sees it disappear immediately.

Passive listeners tell the browser upfront that the handler will never call preventDefault(), so the browser doesn't have to wait for the handler to finish before proceeding with the scroll — improving scroll smoothness, especially on mobile. Since this listener only reads scrollY and toggles a CSS class, it never needs to block the default scroll behavior, so passive: true is free performance with no downside.

evaluateScroll() is called once immediately after the listener is attached, not just inside future scroll events. So if a visitor arrives via a same-page anchor link or the browser restores a scroll position, the cue is shown or hidden correctly on the very first paint rather than waiting for the next scroll event to correct it.

It's a real <button> with a click listener that calls scrollIntoView({ behavior: "smooth" }) on the hero's next sibling section — so it functions as an actual scroll control, not only a visual hint. It also carries an aria-label for screen reader users, since an unlabeled icon-only button would otherwise be meaningless to assistive tech.

Attach the scroll listener in a mount effect (useEffect with an empty dependency array in React) and remove it in the cleanup function to avoid leaking listeners across re-renders. Keep the visibility state in a piece of component state (or toggle a class via a ref) driven by the same scrollY-vs-threshold check, and call evaluateScroll() once synchronously after mount for the same reason as the vanilla version.