How to Build a Scrollytelling Map Experience

How to Build a Scrollytelling Map Experience

Scrollytelling map experiences combine storytelling with interactive maps, revealing map elements and information as users scroll. This engaging format creates immersive narratives that guide users through geographic stories, making complex information accessible and compelling.

In this guide, we'll explore how to build scrollytelling map experiences using scroll-triggered animations and map interactions.

Why Use Scrollytelling?

Scrollytelling offers several benefits:

Generate vector dotted maps

Create vector dotted maps with custom options and download them as SVG or PNG files

Basic Structure

HTML Structure

<div class="scrollytelling-container">
  <div class="map-container">
    <svg class="map" viewBox="0 0 1000 600">
      <!-- Map elements -->
    </svg>
  </div>
  
  <div class="story-sections">
    <section class="story-step" data-step="1">
      <div class="content">
        <h2>Step 1 Title</h2>
        <p>Story content for step 1</p>
      </div>
    </section>
    
    <section class="story-step" data-step="2">
      <div class="content">
        <h2>Step 2 Title</h2>
        <p>Story content for step 2</p>
      </div>
    </section>
  </div>
</div>

CSS Layout

.scrollytelling-container {
  position: relative;
  height: 100vh;
}

.map-container {
  position: sticky;
  top: 0;
  height: 100vh;
  width: 100%;
  z-index: 1;
}

.story-sections {
  position: relative;
  z-index: 2;
}

.story-step {
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: 2rem;
}

.content {
  max-width: 500px;
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

Generate vector dotted maps

Create vector dotted maps with custom options and download them as SVG or PNG files

Scroll Detection

Intersection Observer

class ScrollytellingMap {
  constructor() {
    this.steps = document.querySelectorAll('.story-step');
    this.map = document.querySelector('.map');
    this.currentStep = 0;
    this.init();
  }
  
  init() {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const step = parseInt(entry.target.dataset.step);
            this.activateStep(step);
          }
        });
      },
      {
        threshold: 0.5,
        rootMargin: '-20% 0px -20% 0px'
      }
    );
    
    this.steps.forEach(step => observer.observe(step));
  }
  
  activateStep(step) {
    if (step === this.currentStep) return;
    this.currentStep = step;
    this.updateMap(step);
  }
  
  updateMap(step) {
    // Update map based on step
    switch(step) {
      case 1:
        this.showRegion('north-america');
        break;
      case 2:
        this.showRegion('europe');
        break;
      case 3:
        this.showRegion('asia');
        break;
    }
  }
  
  showRegion(region) {
    // Hide all regions
    this.map.querySelectorAll('.region').forEach(region => {
      region.style.opacity = '0.3';
    });
    
    // Show target region
    const target = this.map.querySelector(`.${region}`);
    if (target) {
      target.style.opacity = '1';
      target.style.transition = 'opacity 0.5s ease';
    }
  }
}

Progressive Reveal

Reveal Elements on Scroll

function revealMapElements(step) {
  const elements = {
    1: ['.region-1', '.marker-1'],
    2: ['.region-2', '.marker-2', '.route-1'],
    3: ['.region-3', '.marker-3', '.route-2']
  };
  
  const toReveal = elements[step] || [];
  
  toReveal.forEach(selector => {
    const element = document.querySelector(selector);
    if (element) {
      element.style.opacity = '0';
      element.style.transform = 'scale(0)';
      
      setTimeout(() => {
        element.style.transition = 'all 0.5s ease';
        element.style.opacity = '1';
        element.style.transform = 'scale(1)';
      }, 100);
    }
  });
}

Animate Routes

function animateRoute(routeElement, step) {
  if (step === 2) {
    const pathLength = routeElement.getTotalLength();
    routeElement.style.strokeDasharray = pathLength;
    routeElement.style.strokeDashoffset = pathLength;
    routeElement.style.transition = 'stroke-dashoffset 2s ease';
    routeElement.style.strokeDashoffset = '0';
  }
}

Map Transformations

Zoom on Scroll

function updateMapZoom(step, scrollProgress) {
  const zoomLevels = {
    1: 1,
    2: 1.5,
    3: 2,
    4: 1
  };
  
  const targetZoom = zoomLevels[step] || 1;
  const map = document.querySelector('.map-container');
  
  map.style.transform = `scale(${targetZoom})`;
  map.style.transition = 'transform 0.5s ease';
}

Pan on Scroll

function updateMapPan(step, scrollProgress) {
  const panPositions = {
    1: { x: 0, y: 0 },
    2: { x: -200, y: -100 },
    3: { x: 200, y: 100 },
    4: { x: 0, y: 0 }
  };
  
  const target = panPositions[step] || { x: 0, y: 0 };
  const map = document.querySelector('.map-container');
  
  map.style.transform = `translate(${target.x}px, ${target.y}px)`;
  map.style.transition = 'transform 0.5s ease';
}

Advanced Features

Smooth Transitions

function smoothMapTransition(fromStep, toStep) {
  const map = document.querySelector('.map');
  const duration = 1000;
  const startTime = performance.now();
  
  function animate(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const ease = easeInOutCubic(progress);
    
    // Interpolate between steps
    updateMapState(fromStep, toStep, ease);
    
    if (progress < 1) {
      requestAnimationFrame(animate);
    }
  }
  
  requestAnimationFrame(animate);
}

function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

Data Visualization

function updateDataVisualization(step) {
  const data = {
    1: { value: 100, region: 'North America' },
    2: { value: 250, region: 'Europe' },
    3: { value: 300, region: 'Asia' }
  };
  
  const stepData = data[step];
  if (stepData) {
    updateChart(stepData.value);
    updateRegionHighlight(stepData.region);
  }
}

Best Practices

Performance

User Experience

Storytelling

Common Patterns

Regional Reveal

Route Animation

Data Progression

Final Thoughts

Building scrollytelling map experiences creates engaging, immersive narratives that guide users through geographic stories. Whether revealing regions, animating routes, or visualizing data, scrollytelling combines storytelling with interactive maps for compelling user experiences.

Start with a clear narrative structure, implement scroll detection, and create smooth map transitions that respond to user scroll. The result is engaging map experiences that tell stories and present information in an accessible, compelling way.

Ready to build your scrollytelling map? Generate your dotted map and start creating scroll-based map experiences today.