Skip to content
Credit Third-Party Data

Credit Third-Party Data in Maptoolkit Maps JS

The moment you add a layer that is not ours, the attribution line stops being complete. Most open datasets require a credit, and the place for it is the source that carries the data rather than a hand-written string in the control, so that the credit appears and disappears with the layer it belongs to.

const API_KEY = 'YOUR_API_KEY';

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
        center: [10.0, 50.5],
        zoom: 4,
        // compact:false keeps the line expanded at every width. The default collapses it
        // to an "i" button on first drag, which does not satisfy most licence terms.
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        map.addSource('nuts', {
            type: 'geojson',
            data: 'https://gisco-services.ec.europa.eu/distribution/v2/nuts/geojson/NUTS_RG_20M_2021_4326_LEVL_0.geojson',
            // The credit belongs to the source, not to the control. It appears while a
            // layer using this source is visible and disappears with it.
            attribution: '&copy; <a href="https://ec.europa.eu/eurostat/web/gisco" target="_blank" rel="noopener">EuroGeographics</a>'
        });

        // Insert below the basemap's labels so place names stay readable over the fill.
        const firstSymbolId = map.getStyle().layers.find((l) => l.type === 'symbol')?.id;

        map.addLayer({
            id: 'nuts-fill',
            type: 'fill',
            source: 'nuts',
            filter: ['==', ['get', 'EU_STAT'], 'T'],
            paint: { 'fill-color': '#2171b5', 'fill-opacity': 0.25 }
        }, firstSymbolId);

        map.addLayer({
            id: 'nuts-line',
            type: 'line',
            source: 'nuts',
            filter: ['==', ['get', 'EU_STAT'], 'T'],
            paint: { 'line-color': '#2171b5', 'line-width': 1 }
        }, firstSymbolId);

        document.getElementById('toggle').addEventListener('change', (e) => {
            const visibility = e.target.checked ? 'visible' : 'none';
            map.setLayoutProperty('nuts-fill', 'visibility', visibility);
            map.setLayoutProperty('nuts-line', 'visibility', visibility);
        });
    });
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Credit Third-Party Data - Maptoolkit Maps JS</title>
    <meta property="og:description" content="Attach attribution to your own data sources." />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
    <style>
        html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
        #map { width: 100%; height: 100%; }
        #panel {
            position: absolute; top: 10px; left: 10px; z-index: 999;
            background: #fff; border-radius: 6px; box-shadow: 0 0 15px #68686880;
            font: 13px/1.6 system-ui, sans-serif; padding: 10px 12px; max-width: 250px;
        }
        #panel label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
        #panel .note { color: #666; font-size: 12px; margin-top: 6px; }
    </style>
</head>
<body>
<div id="map"></div>
<div id="panel">
    <label><input type="checkbox" id="toggle" checked> Show the third-party layer</label>
    <div class="note">Watch the attribution line change as the layer goes on and off.</div>
</div>
<script>
    const API_KEY = 'YOUR_API_KEY';

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
        center: [10.0, 50.5],
        zoom: 4,
        // compact:false keeps the line expanded at every width. The default collapses it
        // to an "i" button on first drag, which does not satisfy most licence terms.
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        map.addSource('nuts', {
            type: 'geojson',
            data: 'https://gisco-services.ec.europa.eu/distribution/v2/nuts/geojson/NUTS_RG_20M_2021_4326_LEVL_0.geojson',
            // The credit belongs to the source, not to the control. It appears while a
            // layer using this source is visible and disappears with it.
            attribution: '&copy; <a href="https://ec.europa.eu/eurostat/web/gisco" target="_blank" rel="noopener">EuroGeographics</a>'
        });

        // Insert below the basemap's labels so place names stay readable over the fill.
        const firstSymbolId = map.getStyle().layers.find((l) => l.type === 'symbol')?.id;

        map.addLayer({
            id: 'nuts-fill',
            type: 'fill',
            source: 'nuts',
            filter: ['==', ['get', 'EU_STAT'], 'T'],
            paint: { 'fill-color': '#2171b5', 'fill-opacity': 0.25 }
        }, firstSymbolId);

        map.addLayer({
            id: 'nuts-line',
            type: 'line',
            source: 'nuts',
            filter: ['==', ['get', 'EU_STAT'], 'T'],
            paint: { 'line-color': '#2171b5', 'line-width': 1 }
        }, firstSymbolId);

        document.getElementById('toggle').addEventListener('change', (e) => {
            const visibility = e.target.checked ? 'visible' : 'none';
            map.setLayoutProperty('nuts-fill', 'visibility', visibility);
            map.setLayoutProperty('nuts-line', 'visibility', visibility);
        });
    });
</script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create an interactive map that adds a third-party GeoJSON source with its own attribution string, and shows how per-source attribution combines with the basemap credit.

How it works

Attribution belongs on the source. Every source type accepts an attribution string, and MapLibre collects the strings from all sources whose layers are currently visible, de-duplicates them and joins them into one line. Hide the layer and the credit goes with it, which is the behaviour a licence actually asks for: the credit is shown while the data is shown.

Contrast that with customAttribution on the control, which is a fixed string that is always present regardless of what is on the map. That is the right tool for a credit that applies to the whole application, and the wrong one for a layer the reader can switch off, because it leaves a credit standing for data nobody is looking at.

The string is HTML, not text, so a link is a real anchor. Add target="_blank" and rel="noopener", because a licence credit that navigates away from the application is a credit people learn to avoid clicking.

compact: false is the other half of getting attribution right, and it is easy to miss. The default is compact, which renders the line expanded and then collapses it to a small information button the first time someone drags the map. A collapsed credit is not a visible credit. Note also that the attributionControl option object replaces MapLibre’s defaults rather than merging with them, so passing any options without compact falls back to width-dependent behaviour rather than to the documented default.

Both layers go in below the basemap’s first symbol layer, so country names and city labels stay on top of the fill rather than under it. Appending them at the end instead buries every label the moment the overlay is switched on.

Setting visibility to none is what drives the credit off, because MapLibre tracks visible layers rather than loaded sources. Removing the layer works too. Leaving the layer visible at zero opacity does not, and correctly so: the data is still being served.

The dataset here needs a credit, which is the point. GISCO boundaries are free to reuse with attribution to EuroGeographics, and most open data carries a condition like it. Check the terms of anything you add, because “open” rarely means “no conditions”.

Next steps

Where the line sits is worth deciding against your own interface rather than leaving at the default, and moving the control is one option in the constructor. Whatever position you choose, nothing of yours may cover it.

If a wrapper library owns the map instance and does not expose attributionControl, render the credit yourself as an ordinary overlay element that cannot be collapsed, and make sure it stays visible at every screen size.