MapLibre GL JS

Accessing OS NGD API - Features via MapLibre GL JS

MapLibre GL JS is a free and powerful JavaScript library for displaying interactive maps on the web. It's based on Mapbox GL JS and provides a wide range pf features for creating maps with custom styles, markers and interactivity.

What you'll need

  • OS Maps API and OS NGD API - Features added to an API project in the OS Data Hub with an API Key.

  • A text editor like Visual Studio Code or Notepad to edit and save your HTML and JavaScript files

Create a basic map

Step 1: Set Up Your HTML file

  1. Create a new HTML file with a text editor (e.g. Notepad, Visual Studio Code)

  2. Add the basic HTML structure to your file with a placeholder <div> for the map.

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OS NGD API – Features | Template (EPSG:3857) | Maplibre GL JS</title>
    <!--Add the Ordnance Survey Styling-->
    <link rel="stylesheet" href="" />
    <script src=""></script>
    <!--Add the Maplibre GL JSlibraries-->
    <link rel="stylesheet" href="" />
    <script src=""></script>
        /* Set the map container size and style */
        body { margin: 0; padding: 0; }
        #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    <!--Create a div element to hold the map-->
    <div id="map"></div>
    <!--Add your Javascript code below--> 
        // Your Javascript code will go here



Step 2: Insert your API Key and OS NGD Collection

  1. To enable access to OS APIs an API key is required. Inside the <script> tag, add a variable called apiKey, replacing 'INSERT_API_KEY_HERE' with the API key from your project.

  2. Add a variable called collectionId, replacing 'INSERT_COLLECTIONID_HERE' with the collection ID for the desired NGD Feature Type and version (e.g. bld-fts-buildingpart-1).

// Set API Key 
 const apiKey = 'INSERT_API_KEY_HERE';
 const collectionId= 'INSERT_COLLECTIONID_HERE';

Step 3: Add a basemap

  1. To add the OS Maps API, you will need to define the map style using MapLibre GL JS's format. This specifies the source of map tiles, which will be retrieved from OS Maps API in the 'Light' raster tiles style.

  2. Initialize the map object using the maplibregl.Map class to configure the basemap layer and define its properties - container, minZoom, maxZoom, maxBounds, style, center and zoom.

  3. Add navigation controls to the map, excluding the compass button and disabling map rotation.

// Create a map style object using the ZXY service.
    const style = {
    "version": 8,
        "sources": {
            "raster-tiles": {
            "type": "raster",
            "tiles": [`{z}/{x}/{y}.png?key=${apiKey}`],
            "tileSize": 256
         "layers": [{
             "id": "os-maps-zxy",
             "type": "raster",
             "source": "raster-tiles"
     // Initialize the map object.
         const map = new maplibregl.Map({
             container: 'map',
             minZoom: 6,
             maxZoom: 19,
             style: style,
             maxBounds: [
                 [-10.76418, 49.528423],
                 [1.9134116, 61.331151]
             center: [-3.541809, 50.727589],
             zoom: 17
         map.dragRotate.disable(); // Disable map rotation using right click + drag.
         map.touchZoomRotate.disableRotation(); // Disable map rotation using touch rotation gesture.
         // Add navigation control (excluding compass button) to the map.
         map.addControl(new maplibregl.NavigationControl({
             showCompass: false

The above code creates the main map instance using the MapLibre GL JS library where you can specify various properties:

  • container: Defines where the map should be displayed, in this instance it is set to the id of the <div> element.

  • minZoom and maxZoom: Sets the minimum and maximum zoom level for the map. Users will not be able to go beyond these levels.

  • maxBounds: Defines the maximum bounds and restricts panning the map.

  • style: Defines the style of the map, configured via a URL pointing at the style specified.

  • center: Sets the initial center point of the map.

  • zoom: Sets the initial zoom level of the map.

Step 4: Add a OS NGD API - Features Layer

  1. Create an empty GeoJSON placeholder to hold the feature objects called by the OS NGD API - Features.

  2. Create a function called fetchFeatures that fetches the API based on the current map extent (bounding box) by generating a bbox string.

  3. Construct the API request URL to fetch OS NGD data from the OS NGD API - Features. The URL includes the collectionId, bbox and apiKey.

  4. Once the features have been returned in JSON, update the source data of the map's layers to display the features.

let layers = [ 'polygon', 'linestring', 'point' ];

// Create an empty GeoJSON FeatureCollection.
const geoJson = {
    "type": "FeatureCollection",
    "features": []

// Define an asynchronous function to fetch and display the NGD Features API features.
async function fetchFeatures(bounds) {
        // Generate a BBOX string for the map extent.
        const bbox = bounds.toArray().toString();

        // Construct the NGD Features API request URL.
        const url = `${collectionId}/items?&key=${apiKey}&bbox=${bbox}`;

        // Fetch features from the API endpoint.
        const features = await fetch(url).then(response => response.json());

        // Update the source data with the new GeoJSON data.
        layers.forEach((element) => map.getSource(element).setData(features))

Step 5: Load and update features on the map dynamically

  1. Event listeners are triggered when the map loads and finishes moving (panning or zooming) to load and update features based on the map's updated extent. Inside the map.on('load',...) event handler, we add styles for various types of features, including polygons, linestrings and points so that any collectionId specified will render.

  2. Inside the map.on('moveend',...) event handler fetches and updates the features based on the map's current extent.

map.on('load', () => {
    // Add a fill style layer to render polygons on the map.
        "id": "polygon",
        "type": "fill",
        "source": {
            "type": "geojson",
            "data": geoJson
        "layout": {},
        "paint": {
            "fill-color": "rgba(51,136,255,0.3)",
            "fill-outline-color": "#38f"
        "filter": [ "==", "$type", "Polygon" ]

    // Add a line style layer to render linestrings on the map.
        "id": "linestring",
        "type": "line",
        "source": {
            "type": "geojson",
            "data": geoJson
        "layout": {},
        "paint": {
            "line-color": "#38f",
            "line-width": 1
        "filter": [ "==", "$type", "LineString" ]

    // Add a circle style layer to render points on the map.
        "id": "point",
        "type": "circle",
        "source": {
            "type": "geojson",
            "data": geoJson
        "layout": {},
        "paint": {
            "circle-color": "rgba(51,136,255,0.8)",
            "circle-radius": 4,
            "circle-stroke-color": "#fff",
            "circle-stroke-width": 1
        "filter": ["==", "$type", "Point"]

    // Get the visible map bounds (BBOX).
    let bounds = map.getBounds();

    // Initial fetch and display of features.
    // Add event which will be triggered when the map has finshed moving (pan + zoom).
    // Implements a simple strategy to only request data when the map viewport invalidates
    // certain bounds.
    map.on('moveend', function() {
        let bounds1 = new maplibregl.LngLatBounds(bounds.getSouthWest(), bounds.getNorthEast()),
            bounds2 = map.getBounds();

        if( JSON.stringify(bounds) !== JSON.stringify(bounds1.extend(bounds2)) ) {
            bounds = bounds2;

Features within the viewport extent will load initially (first 100 features) and continue to load as you pan and zoom across the map.

What's Next

Congratulations! You've successfully created a map using MapLibre GL JS and added an OS NGD layer using the OS NGD API - Features in a few steps. Continue to explore Ordnance Survey's code examples to learn more about advanced features and functionality such as adding markers, pop-ups, and additional layers.

Last updated