Narrative Scroll Controller

A narrative shell that changes rhythm and emphasis as the story progresses.

This experiment requires full-page rendering.

Open live demo

HTML

<div class="narrative-scroll-controller is-arrival" data-narrative-controller>
  <aside class="narrative-scroll-controller__stage">
    <p class="narrative-scroll-controller__eyebrow">Narrative control</p>
    <p class="narrative-scroll-controller__index" data-scene-index>01</p>
    <h2 data-scene-title>Arrival</h2>
    <p class="narrative-scroll-controller__summary" data-scene-copy>
      Establish the world with clarity before rhythm and contrast begin to accelerate.
    </p>

    <div class="narrative-scroll-controller__progress" aria-hidden="true">
      <span data-scene-progress></span>
    </div>
  </aside>

  <div class="narrative-scroll-controller__story">
    <section
      class="narrative-scene is-active"
      data-scene
      data-scene-id="arrival"
      data-scene-index="01"
      data-scene-title="Arrival"
      data-scene-copy="Establish the world with clarity before rhythm and contrast begin to accelerate.">
      <p class="narrative-scene__eyebrow">Scene 01</p>
      <h3>Arrival</h3>
      <p>
        The narrative opens with generous spacing, low pressure and a broad sense of orientation. Nothing rushes the
        reader. The page simply makes the first frame easy to enter.
      </p>
    </section>

    <section
      class="narrative-scene"
      data-scene
      data-scene-id="tension"
      data-scene-index="02"
      data-scene-title="Tension"
      data-scene-copy="Contrast increases, spacing tightens and the page starts to pull the reader forward.">
      <p class="narrative-scene__eyebrow">Scene 02</p>
      <h3>Tension</h3>
      <p>
        As the story sharpens, the visual rhythm becomes denser. Color contrast rises and the stage shifts from ambient
        to active, making the narrative feel more directional.
      </p>
    </section>

    <section
      class="narrative-scene"
      data-scene
      data-scene-id="pivot"
      data-scene-index="03"
      data-scene-title="Pivot"
      data-scene-copy="A deliberate visual pause gives the reader a moment to reset before the final turn.">
      <p class="narrative-scene__eyebrow">Scene 03</p>
      <h3>Pivot</h3>
      <p>
        Midway through the story, the interface creates a short plateau. Rhythm slows, copy gains breathing room and
        emphasis shifts from momentum to reflection.
      </p>
    </section>

    <section
      class="narrative-scene"
      data-scene
      data-scene-id="resolve"
      data-scene-index="04"
      data-scene-title="Resolve"
      data-scene-copy="The final state restores composure and makes the structure of the journey feel complete.">
      <p class="narrative-scene__eyebrow">Scene 04</p>
      <h3>Resolve</h3>
      <p>
        The closing state does not simply fade out. It gathers the preceding movement into a calmer frame so the ending
        feels intentional rather than abrupt.
      </p>
    </section>
  </div>
</div>

CSS

.narrative-scroll-controller {
  display: grid;
  gap: 22px;
  max-width: 1160px;
  margin: 0 auto;
  padding: 18px;
  border-radius: 34px;
  transition: background 260ms ease, box-shadow 260ms ease;
}

.narrative-scroll-controller.is-arrival {
  background: radial-gradient(circle at top left, rgba(19, 190, 156, 0.2), transparent 42%), #0b1014;
}

.narrative-scroll-controller.is-tension {
  background: radial-gradient(circle at top right, rgba(250, 175, 59, 0.22), transparent 40%), #120d0a;
}

.narrative-scroll-controller.is-pivot {
  background: radial-gradient(circle at center, rgba(255, 255, 255, 0.08), transparent 42%), #101017;
}

.narrative-scroll-controller.is-resolve {
  background: radial-gradient(circle at bottom right, rgba(19, 190, 156, 0.18), transparent 45%), #071112;
}

.narrative-scroll-controller__stage {
  align-self: start;
  padding: 28px;
  border-radius: 28px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(12px);
}

.narrative-scroll-controller__eyebrow,
.narrative-scene__eyebrow {
  margin: 0 0 10px;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: rgba(255, 255, 255, 0.58);
}

.narrative-scroll-controller__index {
  margin: 0 0 10px;
  font-size: clamp(3.2rem, 7vw, 5.6rem);
  line-height: 0.9;
  color: rgba(255, 255, 255, 0.32);
}

.narrative-scroll-controller__stage h2 {
  margin: 0 0 12px;
  font-size: clamp(2rem, 4vw, 3.3rem);
  line-height: 0.94;
}

.narrative-scroll-controller__summary {
  margin: 0;
  max-width: 34ch;
  color: rgba(255, 255, 255, 0.8);
  line-height: 1.72;
}

.narrative-scroll-controller__progress {
  height: 4px;
  margin-top: 22px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}

.narrative-scroll-controller__progress span {
  display: block;
  width: 25%;
  height: 100%;
  background: linear-gradient(90deg, #faaf3b, #13be9c);
  transition: width 220ms ease;
}

.narrative-scroll-controller__story {
  display: grid;
  gap: 18px;
}

.narrative-scene {
  min-height: 70vh;
  padding: 30px;
  border-radius: 28px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.04);
  opacity: 0.5;
  transform: scale(0.99);
  transition: opacity 220ms ease, transform 220ms ease, border-color 220ms ease;
}

.narrative-scene.is-active {
  opacity: 1;
  transform: scale(1);
  border-color: rgba(255, 255, 255, 0.2);
}

.narrative-scene h3 {
  margin: 0 0 14px;
  max-width: 12ch;
  font-size: clamp(2.2rem, 4vw, 3.8rem);
  line-height: 0.94;
}

.narrative-scene p:last-child {
  max-width: 56ch;
  margin: 0;
  color: rgba(255, 255, 255, 0.84);
  line-height: 1.8;
  font-size: 1.05rem;
}

@media only screen and (min-width: 1040px) {
  .narrative-scroll-controller {
    grid-template-columns: 360px minmax(0, 1fr);
    align-items: start;
  }

  .narrative-scroll-controller__stage {
    position: sticky;
    top: 18px;
    min-height: calc(100vh - 72px);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
}

JavaScript

(function () {
  const root = document.querySelector('[data-narrative-controller]');
  if (!root) return;

  const scenes = Array.from(root.querySelectorAll('[data-scene]'));
  const stageIndex = root.querySelector('[data-scene-index]');
  const stageTitle = root.querySelector('[data-scene-title]');
  const stageCopy = root.querySelector('[data-scene-copy]');
  const progress = root.querySelector('[data-scene-progress]');
  if (!scenes.length || !stageIndex || !stageTitle || !stageCopy || !progress) return;

  function activateScene(scene) {
    scenes.forEach(item => {
      item.classList.toggle('is-active', item === scene);
    });

    root.classList.remove('is-arrival', 'is-tension', 'is-pivot', 'is-resolve');
    root.classList.add(`is-${scene.dataset.sceneId}`);

    stageIndex.textContent = scene.dataset.sceneIndex;
    stageTitle.textContent = scene.dataset.sceneTitle;
    stageCopy.textContent = scene.dataset.sceneCopy;

    const index = scenes.indexOf(scene);
    progress.style.width = `${((index + 1) / scenes.length) * 100}%`;
  }

  const observer = new IntersectionObserver(
    entries => {
      let nextScene = null;
      let highestRatio = 0;

      entries.forEach(entry => {
        if (entry.intersectionRatio > highestRatio) {
          highestRatio = entry.intersectionRatio;
          nextScene = entry.target;
        }
      });

      if (nextScene) {
        activateScene(nextScene);
      }
    },
    {
      threshold: [0.2, 0.4, 0.6]
    }
  );

  scenes.forEach(scene => observer.observe(scene));
  activateScene(scenes[0]);
})();
  • Tech used Vanilla JS, WordPress-ready, No dependencies
  • Integration level Advanced (custom logic)
  • Performance safe Yes

What it solves

Helps long-form storytelling maintain momentum and orientation without relying on abrupt transitions or heavy-handed effects.

Narrative Scroll Controller is a showcase-level storytelling pattern where the page shifts tone, pacing and emphasis as the story advances. A sticky stage keeps the reader oriented while each narrative block updates the visual state in a measured, editorial way.

The effect is immersive without becoming theatrical. It guides attention, but still lets the content remain legible and deliberate across the whole arc.