Back to blog
Jul 06, 2025
15 min read

Building an Interactive Web Map Application: A Complete Tutorial

Learn how to build an interactive web map application from scratch, featuring project markers, filtering, and detailed views using JavaScript and Mapbox

Interactive Web Map Application Demo

This comprehensive tutorial will guide you through building an interactive web map designed to showcase a portfolio of development projects. The application allows users to explore project locations on a map, filter them by criteria like type or status, and click on individual custom markers to view a details panel with more information. The entire application is orchestrated by a central script that pieces together the map, data, and user interface components.

Application Architecture Overview

Our application consists of several interconnected components:

flowchart TD
    A0["Configuration"]
    A1["Project Data (GeoJSON)"]
    A2["Map View"]
    A3["Filtering Logic"]
    A4["Custom Markers"]
    A5["Details Panel"]
    A6["Application Orchestrator"]
    A6 -- "Uses" --> A0
    A6 -- "Loads" --> A1
    A4 -- "Are added to" --> A2
    A3 -- "Shows/Hides" --> A4
    A4 -- "Shows details in" --> A5

Table of Contents

  1. Chapter 1: Map View
  2. Chapter 2: Project Data (GeoJSON)
  3. Chapter 3: Custom Markers
  4. Chapter 4: Filtering Logic
  5. Chapter 5: Details Panel
  6. Chapter 6: Application Orchestrator
  7. Chapter 7: Configuration

Chapter 1: Map View

Welcome to the National Project Portfolio! This tutorial will guide you through the core concepts of how this interactive map application is built. Let’s start with the most fundamental piece: the map itself.

Imagine you want to display your favorite vacation spots. You could write them down in a list, but that’s not very exciting. A much better way would be to get a big corkboard map and stick a pin in each location. This gives you immediate visual context. Where are the spots in relation to each other? Are they clustered on the coast?

In our application, the Map View is that digital corkboard map. It’s the foundational canvas that provides the geographical background. All of our project “pins” will be placed on this canvas.

What is the Map View?

The Map View is the core visual component of our application. It’s responsible for a few key things:

  • Displaying the World: It renders a beautiful, detailed map that users can explore.
  • Handling Interaction: It allows users to pan (move the map side-to-side) and zoom (get a closer or wider view).
  • Providing a Canvas: It acts as the base layer upon which we will later add our project markers and other interactive elements.

Our project doesn’t build this entire mapping system from scratch. Instead, we use a powerful service called Mapbox. Think of Mapbox as a company that provides all the tools and data needed to add high-quality maps to websites and applications. Our MapView component is our project’s way of setting up and managing the map that Mapbox provides.

Creating Our First Map

To get a map on the screen, we first need an HTML element to hold it. In our index.html file, there’s a simple <div> with an ID of map.

<!-- index.html -->
<div id="map"></div>

This is like reserving a space on our wall for the corkboard.

Next, our main JavaScript file (assets/js/main.js) kicks everything off. It creates a new MapView and tells it where to appear.

// assets/js/main.js

// 1. Get our Mapbox "password" from the configuration.
const accessToken = config.mapboxAccessToken;

// 2. Create a new map instance.
const mapView = new MapView({
  mapContainer: 'map', // The ID of our HTML div
  mapboxAccessToken: accessToken,
  center: [-98.5795, 39.8283], // Center on the USA
  zoom: 3.5 // A good starting zoom level
});

Let’s break this down:

  • We create a new MapView object. This is our custom component that makes using Mapbox easier.
  • We pass it some options:
    • mapContainer: 'map': This tells our MapView to render inside the <div id="map">.
    • mapboxAccessToken: This is a special key that gives us permission to use Mapbox’s services.
    • center and zoom: These properties set the initial view of the map when the page loads. Here, we’re centered on the United States.

After this code runs, you’ll see a beautiful, interactive map of the United States on the webpage. You can already click and drag to pan around and use your scroll wheel to zoom in and out!

How It Works Under the Hood

You might be wondering what happens when we call new MapView(...). How does that simple command turn into a fully interactive map?

Our MapView class is a “wrapper” around the Mapbox library. It simplifies the process and tailors it for our project’s needs. Here’s a step-by-step look at the process.

Let’s peek inside our MapView class in assets/js/map.js to see the most important part.

// assets/js/map.js

class MapView {
  constructor(options) {
    // Tell the Mapbox library what "password" to use
    mapboxgl.accessToken = options.mapboxAccessToken;

    // Create the actual map from the Mapbox library
    this.map = new mapboxgl.Map({
      container: options.mapContainer,
      style: 'mapbox://styles/mapbox/light-v11', // A clean, light style
      center: options.center,
      zoom: options.zoom
    });
  }
}

As you can see, our MapView class is quite simple right now. It takes our clean, easy-to-read options and translates them into the format the Mapbox library expects.

Chapter 2: Project Data (GeoJSON)

Now that we’ve set up our “digital corkboard” – a beautiful, interactive map – we need to add our projects. This raises a crucial question: how do we store all the information for each project in a way that our application can understand?

This is where our project’s “master file” comes in: projects.geojson.

What is GeoJSON?

Imagine you have a rolodex or a box of index cards. Each card represents one project. On that card, you write down all the important details: the project’s name, its status (e.g., “In Progress”), the year it was completed, and a short description. Critically, you also write down its address or map coordinates so you know where to place a pin on your corkboard.

GeoJSON is a standard format for organizing this kind of geographical data. It’s essentially a specially structured text file that acts as our digital rolodex. Our entire application reads from this single file to get all the information it needs to display projects on the map.

The file is named projects.geojson and it lives in the assets/data/ folder.

The Anatomy of a Single Project

Let’s look at one “index card” from our GeoJSON file. In GeoJSON terms, each project is called a Feature. A Feature has two main parts:

  1. geometry: This tells the map where the project is located.
  2. properties: This holds all the other information about the project.

Here’s a simplified example of a single project Feature:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [-118.2437, 34.0522]
  },
  "properties": {
    "name": "Downtown Tower Renovation",
    "status": "Completed",
    "year": 2023,
    "description": "A major overhaul of a historic skyscraper."
  }
}

Let’s break this down:

  • "type": "Feature": This just tells any program reading the file, “Hey, this block of text describes a single thing.”
  • geometry:
    • "type": "Point": We’re marking a single spot on the map, not a line or a shape.
    • "coordinates": [-118.2437, 34.0522]: This is the most important part for mapping! It’s the project’s exact location in [longitude, latitude] format.
  • properties: This is a simple list of key-value pairs, just like a dictionary or an index card.

The Master File: A Collection of Features

Our actual projects.geojson file contains a list of all our projects. It wraps all the individual Feature objects into a larger object called a FeatureCollection.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { ... },
      "properties": { "name": "Project Alpha", ... }
    },
    {
      "type": "Feature",
      "geometry": { ... },
      "properties": { "name": "Project Beta", ... }
    }
  ]
}

How Does the Application Use This Data?

Our application starts by loading and reading this projects.geojson file. Here’s the simplified code in main.js:

// A simplified look at main.js

async function initializeApp() {
  // 1. Create the map (from Chapter 1)
  const mapView = new MapView(...);

  // 2. Fetch the project data from our file
  const response = await fetch('assets/data/projects.geojson');
  const projectData = await response.json();

  // 'projectData' now holds our entire FeatureCollection!
  console.log(projectData.features.length + " projects loaded!");
}

initializeApp();

Chapter 3: Custom Markers

Now we have our map and our data, but they’re not connected yet. How do we take each project from our data file and place a pin for it on the map? And more importantly, how can we make those pins instantly meaningful? This is where Custom Markers come in.

Pins on a Digital Map

Custom Markers are our digital, color-coded pins. They are the visual icons on the map that represent each project. Their appearance—like their color or icon—is determined by the project’s properties, giving you at-a-glance information.

Creating a Marker for Each Project

The basic idea is simple: we need to go through our list of projects one-by-one and, for each one, create a marker and add it to the map.

// A simplified look at main.js

async function initializeApp() {
  // ... map creation and data fetching from previous chapters ...
  const projectData = await loadProjectData();

  // 1. Create a manager for our markers.
  const markerManager = new MarkerManager(mapView.map);

  // 2. Tell the manager to add markers for all our projects.
  markerManager.addMarkers(projectData.features);
}

initializeApp();

How It Works: From Data to a Visual Pin

Let’s peek inside assets/js/markers.js to see how we create a single marker:

// Simplified from assets/js/markers.js

// This function creates one marker for one project.
function createAndAddMarker(project, map) {
  // Step 1: Create a blank HTML div element.
  const el = document.createElement('div');
  el.className = 'marker'; // Give it a standard class for styling

  // Step 2: Determine the icon based on the project's type.
  const iconUrl = getIconForType(project.properties.type);
  el.style.backgroundImage = `url(${iconUrl})`;

  // Step 3: Use the Mapbox library to create and place the marker.
  new mapboxgl.Marker(el)
    .setLngLat(project.geometry.coordinates) // Set its location
    .addTo(map); // Add it to the map
}

The getIconForType function is a simple helper that returns the correct image path:

// A helper function inside markers.js
function getIconForType(type) {
  if (type === 'Commercial') {
    return 'assets/images/markers/marker-commercial.png';
  } else if (type === 'Residential') {
    return 'assets/images/markers/marker-residential.png';
  } else {
    return 'assets/images/markers/marker-default.png';
  }
}

Chapter 4: Filtering Logic

Our map is now full of pins, but what if a user only wants to see projects that are “Completed”? Or only “Residential” projects? This is where our Filtering Logic comes in.

The Sieve Analogy

The Filtering Logic is like a digital sieve. When a user selects a filter, like “Status: Completed,” our application takes all the projects and “pours” them through the “Completed” sieve. Only the projects that match this criterion remain visible on the map.

How It Works: From a Click to a Filtered Map

The process starts when a user interacts with a filter control. Let’s follow the journey of filtering for “Completed” projects:

  1. User Action: The user clicks on the “Status” dropdown and selects “Completed.”
  2. Notifying the Logic: The UI tells our Filtering Logic component about this change.
  3. Applying the Filter: The logic goes through every single project we have loaded.
  4. The Check: For each project, it asks: “Does your status property equal ‘Completed’?”
  5. Show or Hide: Based on the answer, it either shows or hides the project’s marker.

The Code Behind the Sieve

// Simplified from assets/js/filters.js

class FilterManager {
  constructor() {
    // Keep track of the current active filters.
    this.activeFilters = {
      status: 'all',
      type: 'all',
      search: ''
    };
  }
}

// This function loops through all projects and decides to show or hide them.
function applyFilters(allProjects, allMarkers) {
  allProjects.forEach(project => {
    const marker = allMarkers[project.properties.id];

    if (projectMatchesFilters(project)) {
      showMarker(marker); // Make it visible
    } else {
      hideMarker(marker); // Make it invisible
    }
  });
}

function projectMatchesFilters(project) {
  const properties = project.properties;
  const filters = this.activeFilters;

  // Check the status filter
  const statusMatch = (filters.status === 'all') || (properties.status === filters.status);

  return statusMatch; // Must pass all checks to be true
}

Chapter 5: Details Panel

When a user clicks on a marker, they want to see more information. This is where the Details Panel comes in.

The Back of the Baseball Card

The Details Panel is like the back of a baseball card. It’s a dedicated part of the user interface that slides into view when a user clicks on a project marker, displaying all the detailed information about that project.

How It Works: From Click to Detailed View

  1. The Click: The user clicks on a specific marker.
  2. The Listener: Each marker has a “listener” that waits for clicks.
  3. The Hand-off: The listener takes the project data and hands it to the DetailsPanel.
  4. Populating the Panel: The panel fills its HTML elements with the project information.
  5. The Reveal: The panel slides into view with an animation.

The Code Behind the Panel

// Simplified from assets/js/markers.js

function createAndAddMarker(project, map, detailsPanel) {
  const el = document.createElement('div');
  // ... code to style the marker ...

  // **The important part!**
  el.addEventListener('click', () => {
    detailsPanel.show(project); // Tell the panel to show this project
  });

  new mapboxgl.Marker(el)
    .setLngLat(project.geometry.coordinates)
    .addTo(map);
}

The panel HTML structure:

<!-- Simplified from index.html -->
<div id="details-panel" class="details-panel">
  <img id="panel-image" src="" alt="Project Image">
  <h2 id="panel-title"></h2>
  <p id="panel-description"></p>
  <button id="close-panel-btn">Close</button>
</div>

And the panel logic:

// Simplified from assets/js/details-panel.js

class DetailsPanel {
  constructor() {
    this.panel = document.querySelector('#details-panel');
    this.title = document.querySelector('#panel-title');
    this.description = document.querySelector('#panel-description');
    this.image = document.querySelector('#panel-image');
  }

  show(project) {
    const props = project.properties;

    // Fill the HTML elements with project data
    this.title.textContent = props.name;
    this.description.textContent = props.description;
    this.image.src = props.imageUrl;

    // Add a class to make the panel slide into view
    this.panel.classList.add('is-visible');
  }
}

Chapter 6: Application Orchestrator

All these components need to work together harmoniously. This is the job of our Application Orchestrator, which lives in main.js.

The Conductor of the Orchestra

Think of main.js as the conductor. It doesn’t play an instrument itself—instead, it initializes and coordinates all the other components.

The Startup Sequence

When you open the application, main.js performs a specific sequence:

  1. Create the Map: First, it creates the Map View.
  2. Load the Data: It fetches the projects.geojson file.
  3. Initialize Components: It creates the MarkerManager, FilterManager, and DetailsPanel.
  4. Wire Everything Together: It connects the components so they can communicate.

A Look at the Conductor’s Score

// Simplified from assets/js/main.js

async function initializeApp() {
  // Step 1: Create the main pieces
  const mapView = new MapView({ /* ... map options ... */ });
  const detailsPanel = new DetailsPanel();

  // Step 2: Get the sheet music
  const response = await fetch('assets/data/projects.geojson');
  const projectData = await response.json();

  // Step 3: Wire everything together
  const markerManager = new MarkerManager(mapView.map);
  markerManager.addMarkers(projectData.features, detailsPanel);

  const filterManager = new FilterManager();
  filterManager.onFilterChange(() => {
    markerManager.filter(filterManager.activeFilters);
  });
}

// Start the entire application!
initializeApp();

Chapter 7: Configuration

Finally, we need to manage settings like API keys and default configurations. This is where our Configuration system comes in.

The Gadget’s Settings Page

Our Configuration file, config.js, is like a settings page for our application. It stores all the essential settings, especially the Mapbox Access Token.

The Template and Your Secret File

Our project handles configuration with a two-file system:

  1. config.template.js: The blueprint showing what settings you need:
// config.template.js
const config = {
  mapboxAccessToken: 'YOUR_MAPBOX_ACCESS_TOKEN_HERE',
};
  1. config.js: Your personal settings file with real values:
// config.js (This file is private!)
const config = {
  mapboxAccessToken: 'pk.eyA1...your...real...key...',
};

Connecting the Settings to the Application

The configuration is loaded first in index.html:

<!-- Simplified from index.html -->
<html>
<body>
  <!-- 1. Load the configuration file FIRST -->
  <script src="config.js"></script>

  <!-- 2. Load the main application script SECOND -->
  <script src="assets/js/main.js"></script>
</body>
</html>

Our application orchestrator can then use these values:

// Simplified from assets/js/main.js

async function initializeApp() {
  // Read the access token from the global config object
  const accessToken = config.mapboxAccessToken;

  // Create the map view and pass the token to it
  const mapView = new MapView({
    mapContainer: 'map',
    mapboxAccessToken: accessToken,
    // ... other options
  });

  // ... rest of the initialization ...
}

Conclusion

Congratulations! You’ve now learned how to build a complete interactive web map application from scratch. You’ve covered:

  • Map View: The foundational canvas using Mapbox
  • Project Data: Organizing information with GeoJSON
  • Custom Markers: Visual representations of your projects
  • Filtering Logic: User controls for data exploration
  • Details Panel: Rich information display
  • Application Orchestrator: Coordinating all components
  • Configuration: Managing settings and API keys

This architecture provides a solid foundation for building sophisticated mapping applications. You can extend this pattern to add features like:

  • User authentication
  • Real-time data updates
  • Advanced filtering options
  • Custom map styles
  • Export functionality

The key principles you’ve learned—separation of concerns, modular design, and centralized orchestration—will serve you well in any web application project.

Happy mapping!