Skip to content
HomeHome
DE
WhatsAppMailPhone
← All articles
Scrollytelling like TikTok: a website you swipe through instead of scrolling past
Design & UX

Scrollytelling like TikTok: a website you swipe through instead of scrolling past

Plain scrolling is boring. How scrollytelling brings the TikTok experience - one moment after another, snapping, interactive - to a website: the right stack, the building blocks (pin, scrub, snap) and the bugs most people trip over, above all the snapping that fights the smooth scrolling.

Eric MengeAuthorEric MengeOwner & web developer at EMIT Solution
Published
Reading timeca. 11 min

In short

  • Scroll storytelling means the page responds to the scroll position like a film to its timeline. Instead of scrolling passively past, you snap from moment to moment - the UX TikTok made famous, brought to a website.
  • For simple effects, CSS scroll-snap and scroll-driven animations are enough. For real, controlled storytelling (pin, scrub, choreographed transitions) there is hardly a way around Lenis for smooth scrolling plus GSAP ScrollTrigger.
  • The most expensive bug: smooth scrolling and snapping fight each other - the page snaps, the easing pushes it back, it snaps again. The fix is velocity- and direction-aware snapping with a deliberate dead zone, not snapping to the nearest point.
  • What delights on desktop ruins performance on the phone. The pin-heavy choreography belongs behind a desktop switch; mobile needs a lean reveal variant. And prefers-reduced-motion is mandatory, not an extra.

The difference between a website you scroll past and one you pull yourself through is the difference between a brochure and a film. TikTok taught a whole generation how this should feel: one moment, then the next, each snapping into place, you swipe on, something happens. This exact experience can be brought to a website - and I built an entire site around it: my own homepage is exactly this kind of scroll film, from the hero to the very end.

This piece is the guide: the frame, the right stack, the building blocks - and above all the bugs most people trip over. Because scroll storytelling looks effortless in the finished demos and is a fight against the physics of the browser in the making.

The frame: from page to feed

Plain scrolling is passive. The content sits there, you move past it, nothing reacts. A feed is active: each section is a self-contained moment that gets full attention before the next arrives. That is the whole trick of TikTok - not the videos, but the snapping. You are never between two pieces of content, always inside one.

Brought to a website, that means: sections that snap into place (snap), elements held in place while you scroll (pin), and animations that are not played but tied to the scroll position (scrub). Together they create the feeling of pulling through a story rather than rolling down a page.

The stack: when CSS is enough and when it isn’t

The reflex to load a library straight away is wrong. Two things modern CSS does on its own:

/* sections snap into place while scrolling */
.feed {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;
  height: 100vh;
}
.feed > section {
  scroll-snap-align: start;
  height: 100vh;
}

And for effects tied to the scroll there are scroll-driven animations - a native CSS timeline that runs on the scroll instead of the clock:

@keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }
.reveal {
  animation: fade-in linear both;
  animation-timeline: view();   /* runs while the element passes through the viewport */
}

That covers simple cases and costs zero JavaScript. The limit is reached quickly: as soon as you want to pin an element across a long scroll distance and animate several things precisely one after another - a card flies in centrally, grows large, shrinks to its grid position, the next follows - you need real control over the timeline. In practice that means GSAP ScrollTrigger, almost always combined with a smooth-scroll library like Lenis, which turns the choppy native scroll wheel into a smooth motion.

import Lenis from 'lenis';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);

const lenis = new Lenis({ duration: 1.4, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));
gsap.ticker.lagSmoothing(0);

These few lines are the foundation: Lenis takes over the scrolling, ScrollTrigger hooks into the same timeline. From here, every section is a timeline.

The three building blocks

Pin holds an element in place while the scroll continues - the stage for a moment. Scrub ties an animation to the scroll position: you drive it forward and back instead of playing it. Together they form the core:

gsap.timeline({
  scrollTrigger: {
    trigger: '#about',
    start: 'top top',
    end: '+=120%',     // 1.2 viewport heights of scroll for this moment
    pin: true,         // section stays put
    scrub: 0.6,        // animation follows the scroll (with 0.6s easing = smooth)
    anticipatePin: 1,  // against the brief jump when pinning
  },
})
  .to('.content', { xPercent: 0, opacity: 1, duration: 0.15 })
  .to({}, { duration: 0.65 })   // deliberate pause: let the moment land
  .to('.content', { xPercent: -30, opacity: 0, duration: 0.20 });

The empty tween in the middle is not a mistake but dramaturgy: a hold phase in which the content simply stands and can be read before it glides out again. Snap finally makes the page lock in between these moments. And this is where the trouble starts.

The pitfalls most people trip over

1. Snapping fights the smooth scrolling

The most expensive bug, and a tricky one, because both parts work on their own. Smooth scrolling eases out - after you let go, the page keeps moving a little. Naive snapping locks onto the nearest point. Both together: the page snaps, the easing pushes it slightly further, the snapping reads that as new motion and snaps again. The result is jitter or a jump back to the previous section.

The solution is not to snap to the nearest point, but to evaluate speed and direction and add a dead zone:

snap: {
  snapTo: (value, self) => {
    const v = self?.getVelocity() ?? 0;
    // Wide dead zone (±30): below this, the easing is ignored.
    if (v > 30) {            // scrolling down -> next point in that direction
      return POINTS.find((p) => p > value + 0.01) ?? POINTS.at(-1);
    }
    if (v < -30) {          // scrolling up -> previous point
      return [...POINTS].reverse().find((p) => p < value - 0.01) ?? POINTS[0];
    }
    // nearly still -> nearest point
    return POINTS.reduce((c, p) => Math.abs(p - value) < Math.abs(c - value) ? p : c);
  },
  duration: { min: 0.25, max: 0.5 },
}

The core is the dead zone of ±30: below this speed the motion counts as an easing artefact and is ignored, not treated as a snap trigger. Only this ended the jump-back for me.

2. Transform leftovers poison every measurement

If you fly elements from their end position to the centre of the screen with the FLIP technique, you measure their position with getBoundingClientRect(). The trap: if the animation runs a second time (after a resize, say), the transform from the last run is still on the element - and you measure a shifted, scaled position instead of the real one. The cards then fly to the wrong place.

// BEFORE each recalculation, remove the transform leftovers, or you measure junk:
gsap.set(cards, { clearProps: 'transform' });
const rect = card.getBoundingClientRect();  // now the real grid position

3. Snap points don’t survive a resize

Snap points are computed from concrete pixel positions. If the layout changes - window resized, font loaded, image loaded late - they no longer match, and the page locks in at absurd spots. The safeguard is to recompute them on every refresh rather than once at start:

ScrollTrigger.create({
  invalidateOnRefresh: true,
  onRefresh: () => computeSnapPoints(),  // recompute layout-dependent values
  snap: { /* ... */ },
});

4. What delights on desktop kills the phone

The hard truth: the pin- and scrub-heavy choreography is a burden on the phone - for performance and for the feel, because it wrestles with native touch scrolling. The clean solution is a real switch. Above desktop width the full staging, below it only lean reveal animations that play once when scrolled into view:

const isDesktop = window.innerWidth >= 1024;
if (isDesktop) initFullChoreography();
else initSimpleReveals();   // gsap.from(el, { y: 40, opacity: 0, ... }) per section

That is not a lazy compromise but the right call: on the phone nobody wants a held stage, they want to scroll through fluidly.

The line: when it delights and when it annoys

Scroll storytelling is an amplifier, not an end in itself. It pays off when there is a story that benefits from guidance - a self-presentation, a product, a reference showcase. It annoys the moment it steps between the user and a simple piece of information. Someone quickly looking for a phone number or a price does not want to be scrolled through a production.

And two things are mandatory, not optional:

@media (prefers-reduced-motion: reduce) {
  /* No pin, no scrub, no fly-ins - content simply there and readable. */
}

First: respect prefers-reduced-motion. Anyone who has set reduced motion - often for good reason, from motion sickness to attention issues - gets the content calm and without pinning. Second: the content must exist and be readable in the HTML, even without a single script running. The animation is the flourish over a page that works without it too - for search engines, for screen readers, and for the case where the JavaScript fails.

Conclusion

The TikTok experience on a website is not magic but three building blocks - pin, scrub, snap - over a smooth scroll foundation. The difference between a demo that impresses and one that jitters in real use is the pitfalls in between: the snapping that fights the easing, the transform leftovers that poison measurements, the snap points that don’t survive a resize, and the desktop choreography you deliberately switch off on mobile. Know these, and you build not a firework of effects but a page people enjoy pulling themselves through - pausing briefly in the right places.

FAQ

What is scroll storytelling (scrollytelling)?+

A technique where a website's content is tied to the scroll position: as you scroll, elements are held in place, animations run in sync with the scroll motion, and the page snaps from section to section. Instead of a static page you scroll past, you get a guided sequence of moments - comparable to swiping through a TikTok feed.

Do I need JavaScript or is CSS enough?+

For simple cases CSS is enough: scroll-snap-type for snapping between sections and scroll-driven animations (animation-timeline: view/scroll) for effects tied to the scroll. As soon as you want to pin an element across a longer scroll distance and choreograph several animations precisely, you need JavaScript - in practice GSAP ScrollTrigger, usually combined with a smooth-scroll library like Lenis.

Why does my scroll snapping jump back to the wrong place?+

Almost always because smooth scrolling and snapping work against each other. The page snaps onto a point, the easing of the smooth-scroll library moves it slightly further, the snapping reads that as new motion and snaps again - it jitters or jumps back. The solution is not to snap to the nearest point, but to evaluate scroll speed and direction and add a dead zone below which nothing snaps at all.

Does scroll storytelling work on smartphones?+

Only with its own slimmed-down variant. The pin- and scrub-heavy desktop choreography costs too much on mobile and often feels wrong against native touch scrolling. What works is switching to the full choreography above a certain width (e.g. 1024px) and showing only lean reveal animations below it.

Is scroll storytelling bad for accessibility or SEO?+

Not inherently, but it takes care. prefers-reduced-motion must be respected - anyone who has set reduced motion should get the content without the animations and without pinning. The content itself must exist and be readable in the HTML (not created only by scrolling), then SEO is unproblematic too. It is also important that you can always keep scrolling and never get stuck.

Want to know more?

In a free intro call we discuss how you can use these topics for your company. Not a sales pitch, but an honest assessment.

Book a free intro call