All docschevron-rightHelpchevron-rightarrow-leftTutorialschevron-rightUse Mapbox GL JS in a Vue app

Use Mapbox GL JS in a Vue app

Intermediate
codeJavaScript
Prerequisite

Familiarity with Vue and front-end development concepts.

Vue is a popular JavaScript web framework that focuses on declarative rendering and component composition. Since Vue manipulates the DOM, like Mapbox GL JS, it is worth spending some time to understand how to combine the two.

In this tutorial, you will learn how to create a Vue web app that uses Mapbox GL JS to render a map, display the coordinates of the map center point and its zoom level, then update the display when a user interacts with the map. This app also shows map bearing and pitch if the map has been rotated or pitched. You will be able to use the principles discussed in this tutorial to create more complex apps that use both Vue and Mapbox GL JS. This tutorial shows code for the Vue Options API and Component Events.

Getting started

To get started, you will need:

  • A Mapbox access token. Your Mapbox access tokens are on your Account page.
  • A text editor. Use the text editor of your choice for writing HTML, CSS, and JavaScript.
  • Node.js and npm. To run the commands necessary to run your Vue app locally, install Node.js and npm.

Set up the Vue app structure

To get started, create a Vue project using npm. This will create a "base layer" to build any Vue app upon. Choose a root directory to place your project within; typically, this is a directory that contains other project folders or repositories. Use cd in your terminal to navigate to this root directory and run the following command:

npm create vue@3.3.4

You will be prompted to install vue. After successful installation, you will be prompted to name the project and asked about some add-ons. Name the project whatever you like; you may safely reject the add-ons.

Then, in your command line, cd into the new Vue project directory and run the following command:

npm install mapbox-gl

This will install the dependencies that your app requires, including Mapbox GL JS. This will create the file package-lock.json and the folder node_modules.

Finally, delete unnecessary files that will clutter your development process. In the src folder, delete all preexisting components. This means deleting all files in src with the .vue file extension except for App.vue. Delete the contents of the components folder, and delete the entire icons folder.

Now you should be set up, and have the following folder structure:

folder use-mapbox-gl-js-with-vue
folder node_modules
folder public
folder src
folder assets
folder components
code App.vue
code main.js
code .gitignore
code index.html
code package-lock.json
code package.json
document README.md
code vite.config.js

Save your changes.

Most of the programming in this tutorial will occur in two files: src/App.vue, and a component called Map.vue which does not yet exist. Open App.vue, and create Map.vue inside of src/components.

Set up global CSS

The first thing to edit isn't in either of these files, but rather some CSS in src/assets/main.css. Open this file.

In the #app selector, edit the first two lines. These lines should be max-width and margin styles. Replace these lines with the following:

display: flex;
height: 100vh;

This will make the map full-screen.

If there is a line defining style rules for padding under the #app selector, delete this rule. Then, below existing CSS rules, add the following rules to the #app selector:

padding: 0;
flex: 1;

If there are any style rules for #app inside of the @media (min-width: 1024px) selector, delete them and replace them completely with the above four rules. Your final main.css file should look like the following:

@import url('./base.css');

#app {
  display: flex;
  height: 100vh;
  padding: 0;
  flex: 1;
}

a,
.green {
  text-decoration: none;
  color: hsl(160deg 100% 37% / 100%);
  transition: 0.4s;
}

@media (hover: hover) {
  a:hover {
    background-color: hsl(160deg 100% 37% / 20%);
  }
}

@media (width >= 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  #app {
    display: flex;
    height: 100vh;
    padding: 0;
    flex: 1;
  }
}

Set up the Vue app

Set up the app itself next. Take a look inside of App.vue. The entirety of the file is HTML elements. Specifically, it begins with <template> tags to use components in the page, <script> tags to import the components, and <style> tags to apply CSS.

Delete any HTML currently in App.vue and replace it with the following:

<template>
  <div id="layout">
    <Map/>
  </div>
</template>

<script>
import Map from './components/Map.vue';
import "../node_modules/mapbox-gl/dist/mapbox-gl.css"

export default {
  components: {
    Map
  },
};
</script>

<style>
#layout {
  flex: 1;
  display: flex;
  position: relative;
}
</style>

In the <script> tags, the import statement consumes the components from another file and the export statement makes this component available to the app. It also imports CSS from Mapbox GL JS. The <template> tags correspond to the elements shown on the page. The <style> tags apply styles to elements in the <template>. Essentially all that this app will do is display the component Map at the viewport's maximum dimensions.

At this point, Map.vue is not rendering anything. Our next steps will be to add an element to serve as the map container and to instantiate a new Mapbox GL JS map.

Set up the Map component

Open the Map.vue file. Typical Vue components include two sections: <template> tags to show / use the component on-screen, and <script> tags that instruct the component how to behave using JavaScript. <style> tags are optional, but are frequently included as a third section to add CSS rules to the component.

Set up your empty component file like this:

<template>
</template>

<script>
</script>

<style>
</style>

Edit the template

We'll add to the <template> section first. You need an HTML element capable as acting as your map's map container. To reference this element in the <script> section, we must also use a ref attribute. Add the following between your <template> tags:

<div ref="mapContainer" class="map-container"></div>

Add CSS

Now, we'll style the map container so that it fills up the entire viewport and responds to resizing of the browser window. In between your opening and closing <style> tags, add the following:

.map-container {
  flex: 1;
}

Add Mapbox GL JS

We can start creating a map by importing mapboxgl and attatching Mapbox your access token; in between your <script> tags, add the following:

import mapboxgl from "mapbox-gl";
mapboxgl.accessToken = YOUR_MAPBOX_ACCESS_TOKEN

The rest of the JavaScript between your <script> tags will define what is exported by this file when the component is imported by other files. Under the assignment of your access token, add the following:

export default {

};

Finally, we can instantiate the Mapbox map. Inside of the default export object, add the following:

mounted() {
  const map = new mapboxgl.Map({
    container: this.$refs.mapContainer,
    style: "mapbox://styles/mapbox/streets-v12", // Replace with your preferred map style
    center: [-71.224518, 42.213995],
    zoom: 9,
  });

  this.map = map;
},

This mounted() event is part of the lifecycle of Vue components and occurs after the component binds to the DOM. After the Map component is mounted, the Mapbox map object is instantiated. Its container points to the <div> we created in the component's <template> section through $refs.

We also give the component a map property in the last line. This saves a reference to the Mapbox map object in this.map. If, for example, you wanted to remove the map in the case of an unmounted() event, you would call remove() on this.map.

Let's do it. Although our app does not intend to unmount with the unmounted() event at any point, it is generally good form to include instructions to handle this event in components that use a mounted() event to avoid a memory leak. Under the mounted() event, add the following:

unmounted() {
  this.map.remove();
  this.map = null;
}

Save all your work. Run npm run dev in a terminal in your app's root directory, and you should see an interactive map of Boston.

Add a user interface

This app will need to be able to store and render the longitude, latitude, and zoom for the map in a UI element. These values will all change as your user interacts with the map, and the UI will update with it.

Add sidebar HTML

It's time to add to the <div> tags that will become the informational sidebar UI element. In App.vue, add the following HTML between your opening <div> tag with id "layout" and the <Map/> component:

<div id="sidebar">
  Longitude: -71.224518 |
  Latitude: 42.213995 |
  Zoom: 9
</div>

This information will not update until we create additional variables and functions in the component, but it is accurate to the starting position of the map. If you load the development server with only this HTML added, the information will likely appear in a white box to the side of the map. This sidebar should instead lay on top of the map. We need to style the sidebar with CSS.

Add sidebar CSS

Before we work on updating the display with current data, we should style the display. Between your <style> tags, under the rules for the layout id, add the following:

#sidebar {
  background-color: rgb(35 55 75 / 90%);
  color: #fff;
  padding: 6px 12px;
  font-family: monospace;
  z-index: 1;
  position: absolute;
  top: 0;
  left: 0;
  margin: 12px;
  border-radius: 4px;
}

This will place the sidebar in the top-left corner of the map in a styled box.

Save your changes.

Update center and zoom information

To pass center and zoom information from the App to the Map component (to be able to reset the location of the map), the component must use props. All this information will be stored in one variable, modelValue.

Open Map.vue. To create such a prop, we can add it in a similar way to the component's data. Inside of export default but above the definition of data, add the following:

props: ['modelValue'],

Make sure that the name of the prop is "modelValue". Then, in the mounted() event, add the following above the instantiation of the map:

const { lng, lat, zoom, bearing, pitch } = this.modelValue

This setup may seem strange at first. Instead of assigning to modelValue the values of lng, lat, etcetera, we use destructuring assignment syntax to assign these values to the values inside the modelValue object. But our prop modelValue doesn't contain anything yet.

Add data to the app

modelValue needs an initial value so that we can assign lng, lat, and so on. That initial value will come from App.vue; open this file.

In the app's export default, add the following section under the components section:

data: () => ({
  location: {
    lng: -71.224518,
    lat: 42.213995,
    bearing: 0,
    pitch: 0,
    zoom: 9
  }
}),

This data will serve as the initial state of modelValue. To connect the location data to the modelValue prop so that it can be delivered to lng, lat, etc in the map component, change your implementation of <Map/> in the <template> section of the app. Replace the current tag with the following tag:

<Map v-model="location" />

Component v-model can be used within Vue to specifically update the modelValue prop. It is one of many Vue directives. Now, the modelValue prop will be updated with the initial value provided in data by the time the component attempts to assign variables like lng and lat.

You should replace the properties of the instantiated map object in Map.vue Replace center: [-71.224518, 42.213995], and zoom: 9, with the following:

center: [lng, lat],
bearing,
pitch,
zoom,

Add event listeners for an emitted event

Now, let's constantly re-assign modelValue to store the current properties of the map. Go back to the Map.vue component file. We can use a Vue emitted event to update the value of modelValue. In the mounted() event, after the Map instantiation but before the assignment this.map = map, add the following:

const updateLocation = () =>
  this.$emit('update:modelValue', this.getLocation())

map.on('move', updateLocation)
map.on('zoom', updateLocation)
map.on('rotate', updateLocation)
map.on('pitch', updateLocation)

This is a good way to constantly pass the state of the map component back to the app so the UI can update. Event listeners are added to the map so that if it is moved in any way (panned, zoomed, rotated, or pitched), the updateLocation function is called. This function updates modelValue with the contents of this.getLocation().

Define a function to get location data

You may have noticed that the function this.getLocation() does not exist yet. We can add our first function to this component by defining a new methods section in export default. Under the unmounted() event, add the following section:

methods: {
  getLocation() {
    return {
      ...this.map.getCenter(),
      bearing: this.map.getBearing(),
      pitch: this.map.getPitch(),
      zoom: this.map.getZoom(),
    }
  }
},

This function uses methods from Mapbox GL JS to return information about the map's position. Notice that the results of the call to getCenter() are unpacked with spread syntax into the two key-value pairs of a LngLat object, lng and lat. The returned object has the same keys as the object assigned to the value of this.modelValue at the beginning of the mounted() event.

Replace UI data with component data

If you launch your app at this point, you may notice that the data in the sidebar element doesn't update as you pan and zoom the map. This is because we never updated the "default" starting text we placed in the element. Go back to App.vue and find the <div> with an id of "sidebar". Delete its contents and replace them with the following:

Longitude: {{ location.lng }} |
Latitude: {{ location.lat }} |
Zoom: {{ location.zoom }} |

Now, the app updates the data in the UI as the map is moved. Save all your work.

The data presented in the UI at this point is far too specific. Upon moving the map, the longitude, latitude, and zoom all show precision of more than 10 decimal points. We should add some functions to the app to clean these up. Alter the contents of the <div> (shown above) to the following:

Longitude: {{ location.lng.toFixed(4) }} |
Latitude: {{ location.lat.toFixed(4) }} |
Zoom: {{ location.zoom.toFixed(2) }} |

The toFixed() function rounds numbers to a specified number of decimal points and can be used to make the map's positional data values look nicer.

All that's needed to make this project complete is conditionally adding bearing and pitch to the UI if they are not set to 0 (as they are by default). Right under the line Zoom: {{ location.zoom }} |, add the following to the sidebar <div>:

<template v-if="location.bearing"> Bearing: {{ location.bearing.toFixed(2) }} | </template>
<template v-if="location.pitch"> Pitch: {{ location.pitch.toFixed(2) }} | </template>

This takes advantage of Vue's conditional rendering by adding a directive that evaluates the truthiness of an expression. In this case, the expression is the value of the map's bearing and pitch respectively, which evaluate to true only if they are nonzero.

Optional: Add a reset button

Though the app does what we originally set out to do, it will be instructive for new Vue developers to add a "reset" button that resets both the data in the UI and the map to the initial state. This section of the tutorial makes use of Vue Watchers and shorthand for Vue directives.

Directly under the code you were just editing (the Pitch <template>), add the following HTML to the sidebar <div>:

<button @click="location = { lng: -71.224518, lat: 42.213995, zoom: 9, pitch: 0, bearing: 0 }">Reset</button>

This adds a button to the UI that, on click, resets the location data to what it was when the map was loaded. Because the sidebar shows what is stored in location, this updates the UI but map does not jump back to its initial position. Although the Map component passes the prop value to mapbox-gl during creation, mapbox-gl is not aware of Vue's reactivity model, so we need to watch for changes from Vue and trigger appropriate mapbox-gl methods in response.

To fix this, we should watch the modelValue prop. watch() watches a data source and invokes a function when the source changes. In this case, the watcher will keep track of when the modelValue prop changes. When it does, it will test to see if modelValue matches the current state of the map as determined by getLocation(), which uses GL JS functions to directly get the map's position. If it does, nothing happens; if it doesn't, the map's position is set to match the values of modelValue.

Add the following to the default export of your Map.vue component, under the unmounted() event and before the methods section:

watch: {
  modelValue(next) {
    const curr = this.getLocation()
    const map = this.map

    if (curr.lng != next.lng || curr.lat != next.lat)
      map.setCenter({ lng: next.lng, lat: next.lat })
    if (curr.pitch != next.pitch) map.setPitch(next.pitch)
    if (curr.bearing != next.bearing) map.setBearing(next.bearing)
    if (curr.zoom != next.zoom) map.setZoom(next.zoom)
  }
},

Now, when the reset button is pressed, modelValue updates to contain the data newly provided to location, and the map realizes it should update to match modelValue.

That's it! Now you have a fully-functioning Mapbox map with custom UI working in Vue.

Final product

You have created a Vue app that uses Mapbox GL JS to render a map, display the center coordinates and the zoom level of the map, and then update that display when a user interacts with the map.

The final App.vue file will look like the following:

<script>
import Map from "./components/Map.vue";
import "../node_modules/mapbox-gl/dist/mapbox-gl.css"

export default {
  components: {
    Map,
  },

  data: () => ({
    location: {
      lng: -71.224518,
      lat: 42.213995,
      bearing: 0,
      pitch: 0,
      zoom: 9,
    },
  }),
};
</script>

<template>
  <div id="layout">
    <div id="sidebar">
      Longitude: {{ location.lng.toFixed(4) }} |
      Latitude: {{ location.lat.toFixed(4) }} |
      Zoom: {{ location.zoom.toFixed(2) }} |
      <template v-if="location.bearing"> Bearing: {{ location.bearing.toFixed(2) }} | </template>
      <template v-if="location.pitch"> Pitch: {{ location.pitch.toFixed(2) }} | </template>
      <button @click="location = { lng: -71.224518, lat: 42.213995, zoom: 9, pitch: 0, bearing: 0 }">Reset</button>
    </div>
    <Map v-model="location" />
  </div>
</template>

<style>
#layout {
  flex: 1;
  display: flex;
}

#sidebar {
  background-color: rgb(35 55 75 / 90%);
  color: #fff;
  padding: 6px 12px;
  font-family: monospace;
  z-index: 1;
  position: absolute;
  top: 0;
  left: 0;
  margin: 12px;
  border-radius: 4px;
}
</style>

The final Map.vue file will look like the following:

<template>
  <div ref="mapContainer" class="map-container"></div>
</template>

<script>
import mapboxgl from "mapbox-gl";
mapboxgl.accessToken = YOUR_MAPBOX_ACCESS_TOKEN;

export default {
  props: ["modelValue"],

  data: () => ({ map: null }),

  mounted() {
    const { lng, lat, zoom, bearing, pitch } = this.modelValue;

    const map = new mapboxgl.Map({
      container: this.$refs.mapContainer,
      style: "mapbox://styles/mapbox/streets-v12",
      center: [lng, lat],
      bearing,
      pitch,
      zoom,
    });

    const updateLocation = () =>
      this.$emit("update:modelValue", this.getLocation());

    map.on("move", updateLocation);
    map.on("zoom", updateLocation);
    map.on("rotate", updateLocation);
    map.on("pitch", updateLocation);

    this.map = map;
  },

  unmounted() {
    this.map.remove();
    this.map = null;
  },

  watch: {
    modelValue(next) {
      const curr = this.getLocation();
      const map = this.map;

      if (curr.lng != next.lng || curr.lat != next.lat)
        map.setCenter({ lng: next.lng, lat: next.lat });
      if (curr.pitch != next.pitch) map.setPitch(next.pitch);
      if (curr.bearing != next.bearing) map.setBearing(next.bearing);
      if (curr.zoom != next.zoom) map.setZoom(next.zoom);
    },
  },

  methods: {
    getLocation() {
      return {
        ...this.map.getCenter(),
        bearing: this.map.getBearing(),
        pitch: this.map.getPitch(),
        zoom: this.map.getZoom(),
      };
    },
  },
};
</script>

<style>
.map-container {
  flex: 1;
}
</style>

Next steps

Now that you have created a Vue app that uses Mapbox GL JS, you can explore examples using other JavaScript frameworks, such as React or Svelte.

Was this page helpful?