Scroll Testimonial Sequence — Free Sticky Quote Story (No Library)

Scroll Testimonial Sequence · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Zero dependencies
Built entirely on native IntersectionObserver, no carousel or animation library.
Single stable card
One sticky element is rewritten in place, so its position never jumps between swaps.
Deliberate swap timing
A brief opacity dip gives each testimonial its own distinct beat.
Middle-band trigger
Swaps fire when a context label is centered in the viewport, not just visible.
Synchronized dot indicator
Tracks the active testimonial from the same observer callback.
Text-based star ratings
Renders any rating from a single sliced string, no icon assets.
Fully reversible
Scrolling up swaps testimonials back in the correct reverse order.
Responsive fallback
Sticky card hides and context labels read as a plain list on narrow viewports.

About this UI Snippet

Scroll Testimonial Sequence — A Sticky Quote Card Narrated by Scroll Position

Screenshot of the Scroll Testimonial Sequence snippet rendered live

A testimonial carousel needs a visitor to click through it. This snippet instead applies the scrollytelling pattern to social proof: a sticky quote card stays pinned on one side while short context labels scroll past on the other, and the card's content swaps automatically the moment each context label reaches the reader's eye-line — no click, no autoplay timer, no dependency.

One sticky card, swapped by index, not four separate cards

Rather than stacking four full testimonial cards and cross-fading between them, there is exactly one .tss-card in the DOM. applyTestimonial(index) rewrites its quote, star rating, name, role, and avatar initials in place from a single TESTIMONIALS array. This keeps the sticky element's height and position completely stable across every swap — nothing resizes or reflows, which matters because position: sticky elements can visually jump if their own height changes mid-scroll.

A brief opacity dip gives the swap a beat

Text is not simply overwritten instantly; applyTestimonial first fades the whole card to opacity: 0, waits 160ms, rewrites every field, then fades back to 1. That deliberate pause is what makes the swap read as "the next testimonial" rather than a jarring instant text replacement — a small but important detail for content that's meant to feel like distinct voices, not a single scrolling paragraph.

Middle-band detection drives both text and the active dot

The same IntersectionObserver pattern used elsewhere in this library's scrollytelling snippets — rootMargin: '-45% 0px -45% 0px' — watches the four narrow .tss-step context labels. Whichever one is centered in the viewport both triggers applyTestimonial and toggles that label's own is-active styling, so the context text on the left and the swapped quote on the right always change at the exact same scroll moment.

Star ratings render from a single template string

starsEl.textContent is built by slicing a ten-character string of five filled and five empty stars at an offset derived from the rating — a compact way to render "4 out of 5 stars" or "5 out of 5 stars" as plain text glyphs with no icon font, SVG, or conditional loop needed.

Customizing it

Add a fifth testimonial and a matching .tss-step with the next data-index — the observer and dot generator both read the list length dynamically. Swap the star-glyph rendering for real SVG icons if a heavier visual is wanted, or pair with a scroll stat reveal story as a metrics-first companion section right before the testimonials.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the testimonial card rewrites a single element's content in place instead of cross-fading between multiple stacked cards, and why the brief opacity dip before each swap matters for making four different testimonials read as distinct voices rather than one continuously changing paragraph. The same assistant can help extend the pattern — ask it to add a small company-logo badge that swaps alongside each testimonial, sync a background color or accent shift per testimonial the way scroll color sections do, or add autoplay-on-idle behavior that advances the sequence if the visitor stops scrolling for a while. Treat it as a working, dependency-free base for your own scroll-driven social proof section.

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 "scroll testimonial sequence" in plain HTML, CSS, and JavaScript using only the native IntersectionObserver API — no external library, no GSAP, no bundler, no carousel plugin.

Requirements:
- A two-column layout: a left column of several short, minimal context labels (one per testimonial, describing the person's role and company), each with a data-index attribute matching its position in a testimonials array; and a right column containing a single sticky card (position: sticky, top: 0, height: 100vh) showing a quote, a star rating, an avatar with initials, a name, and a role.
- Store all testimonial content — quote text, star rating, name, role, and avatar initials — in a single JavaScript array, and write a function that rewrites the one sticky card's content in place from whichever array entry is currently active, rather than creating or toggling between multiple separate card elements.
- Before rewriting the card's content on each swap, briefly fade the card's opacity to 0, wait a short delay (around 150-200 milliseconds), then rewrite the text fields and fade the opacity back to 1, so each testimonial swap has a deliberate visual beat rather than an instant text replacement.
- Render the star rating as plain text star glyphs (filled versus empty) derived directly from a numeric rating value, without using an icon font, SVG icons, or external image assets.
- Use a single IntersectionObserver with a rootMargin that narrows its detection zone to a band across the vertical middle of the viewport (such as -45% top and bottom) watching all the context label elements, and call the card-update function with the intersecting label's index whenever a label becomes active in that middle band.
- Add a row of small dot indicators, one per testimonial, and keep exactly one marked active at all times, synchronized from the same IntersectionObserver callback that swaps the card content.
- Ensure scrolling back up through the context labels re-triggers earlier testimonials in the correct reverse order, with the same fade-swap behavior, entirely from the observer's own bidirectional intersection tracking.

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 the HTML, CSS, and JSNo CDN scripts needed — everything runs on native browser APIs.
  2. 2
    Scroll into the testimonial sectionThe sticky card shows the first testimonial while its context label is active.
  3. 3
    Keep scrollingEach new context label swaps the card's quote, rating, and person in place.
  4. 4
    Watch the dot indicatorIt tracks which testimonial is currently showing.
  5. 5
    Scroll back upEarlier testimonials swap back in as their context label re-enters the middle band.
  6. 6
    Edit the TESTIMONIALS arrayQuotes, ratings, names, and roles all drive from one array.

Real-world uses

Common Use Cases

SaaS marketing and pricing pages
Narrate multiple customer voices without a click-driven carousel.
Case study landing pages
Pair each testimonial with a specific outcome or metric as scroll context.
Sales enablement microsites
Walk prospects through varied customer profiles who solved similar problems.
Agency or consultancy portfolios
Show client feedback tied to distinct project types.
App store or product landing pages
Reveal reviews from different user segments as visitors scroll features.
Investor or partner-facing pages
Present customer validation as a paced narrative rather than a static grid.

Got questions?

Frequently Asked Questions

Rewriting one card's content in place, rather than cross-fading between four separately sized cards, guarantees the sticky element's height and position never shift between testimonials. Since position: sticky elements can visibly jump if their own box size changes mid-scroll, keeping a single fixed-layout card and only swapping its text content avoids that entirely.

applyTestimonial sets the card's opacity to 0, waits 160 milliseconds, rewrites every text field, then restores opacity to 1. That short, deliberate pause is what makes each testimonial swap read as a distinct new voice rather than a jarring instant text replacement — an instant overwrite would make four different people's quotes feel like one continuously mutating paragraph.

A single IntersectionObserver watches all four narrow .tss-step context labels using a rootMargin of '-45% 0px -45% 0px', which narrows its detection zone to the vertical middle band of the viewport. Whichever context label is centered there triggers applyTestimonial with that label's data-index, so the swap always happens right as the reader's eyes reach that label — the same middle-band technique used across this library's other scrollytelling snippets.

Add a new entry to the TESTIMONIALS array with a quote, stars, name, role, and initials, and add a matching .tss-step element in the HTML with data-index="4". The dot generator and the IntersectionObserver both derive their behavior from the TESTIMONIALS array length and the live set of .tss-step elements respectively, so no other JavaScript needs to change.

Create the IntersectionObserver inside a mount effect after the step elements have rendered, and store the active testimonial index in component state rather than writing to the DOM directly — render the quote, stars, name, and role from that state so the framework's own re-render handles the swap, and apply the fade timing with a CSS transition on opacity tied to a "swapping" state flag. Disconnect the observer in the cleanup function.