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: '© <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: '© <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.
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.