Parallax storytelling — proof of concept
A scroll-driven parallax experience exploring layered narrative on the AEO marketing site.

- POC → pattern
- Proved viable for AEO editorial
- Editorial-driven
- Per-section narrative content
- Layered
- Foreground / background parallax
Problem
In the early-to-mid 2010s, scroll-driven parallax was emerging as a way to give marketing pages a sense of depth and narrative without the production cost of video. AEO's editorial team wanted to explore what that storytelling style could look like for the brand, without committing to building it production-grade until they'd seen it move.
Approach
I built a proof-of-concept parallax framework: a vertical-scroll experience with layered foreground/background elements that move at different rates, paired with breadcrumb navigation that adapts to the user's position in the narrative.
The implementation focuses on a few things that matter for any parallax work:
- Performant scroll handling — transforming layers rather than triggering layout
- Anchor-driven section transitions so the framework can drive opacity, position, and timing per section
- A scaffolding pattern that lets editorial decide what each section contains rather than baking in a specific narrative
Creative engineering work often lives or dies on the prototype. Proof-of-concept builds aren't throwaway — they're the artifact stakeholders need before they'll greenlight production work. The bet here was that a small, polished demo would unlock conversations that pitch decks couldn't.
The demo is the original parallax framework — scroll through it to see the layered transitions and section navigation.
Technical highlights
This codebase is 85 lines of user code — the proof-of-concept layer of a parallax framework. Honest cap is 5/10; nothing here pushes into the 7+ tier.
Any scroll-driven interface has the same race condition: the user clicks a navigation control that triggers a programmatic scroll animation, then during that animation they touch the wheel or trackpad — and now two scroll sources are fighting. The "fix" most implementations ship is wrong (cancel the user's input). The right answer is to give the user priority, always. This requires the programmatic animation to listen for its own preemption signals.
// User-driven scroll: cancel any in-flight programmatic animation immediately.
body.on("scroll DOMMouseScroll mousewheel", function (evt) {
if (evt.type === "mousewheel" || evt.type === "DOMMouseScroll") {
body.stop(true);
}
});
// Breadcrumb click: kick off a distance-scaled animation, but stop any prior one first.
paraCrumb.on("click", function () {
body.stop();
var scrollToAnchor = $(this).attr("href");
var scrollToPos = $(scrollToAnchor).offset().top;
body.animate(
{ scrollTop: scrollToPos + "px" },
(Math.abs(scrollToPos - Math.abs(scrollPos)) / 800) * 1000
);
});Two cooperative pieces: (1) the wheel/touch listener calls body.stop(true) to abort any programmatic animation the moment user input arrives, and (2) the breadcrumb handler also calls body.stop() before starting its own, so back-to-back clicks don't queue up an animation backlog. The duration is computed from distance / 800 * 1000 — far clicks take longer than near clicks, which keeps perceived velocity consistent regardless of where you're jumping.
Skrollr drives scroll-based effects through data attributes (data-anchor-target, data-center, data-bottom-top, etc.) — but writing them by hand for every section is repetitive. This walks every section in the page and emits a breadcrumb element with the right attributes for that anchor. The breadcrumb's visual state then becomes a pure function of scroll position: when the page hits that section's center, the breadcrumb turns red; otherwise it's white.
_.each(anchors, function (anchor) {
var anchorId = $(anchor).attr("id");
$(paraCrumbs).append(
'<div class="paraCrumb" data-anchor-target="#' + anchorId + '"' +
' data-bottom-top="background-color: rgba(255,255,255,.6);"' +
' data-center="background-color: rgba(204,36,41,.6);"' +
' data-top-bottom="background-color: rgba(255,255,255,.6);">' +
"</div>"
);
});The leverage is that the declarative attributes do the rest — Skrollr interpolates between states automatically as the user scrolls. The generator only has to know the anchor IDs and the state map; it doesn't have to know anything about the scroll position or how the interpolation works.