Every map you've ever seen on a competitor's app uses the same default style. The generic beige roads, the mid-teal water, the cluttered POI icons that fight for attention with your own UI, these are the universal defaults, designed to offend nobody and delight nobody. They communicate one thing clearly: this product is using a map library, not owning its map experience.
Custom map styling changes that. A dark mode map on a logistics dashboard, a brand-green road network on a real estate app, a clean minimalist style with your logo where the attribution watermark used to be, these are details that elevate a tool into a product.
This tutorial walks through the Mapbox Style Specification as implemented by MapAtlas, from the structure of the style JSON down to a complete light/dark toggle in React. You don't need GIS experience. You need to be comfortable with JSON and JavaScript.
If you haven't yet set up a basic map, start with How to Add Interactive Maps to Your Website first, then come back here for styling.
Why Default Map Styles Look Generic
A tile provider's default style is designed to be universally legible for the widest possible audience, in the widest possible context. It prioritizes informational density over aesthetic opinion. Roads need to be visible. Labels need to contrast. POI icons need to be recognizable.
None of those goals align with your product's goals. Your app probably cares about a small subset of what's on the map, delivery routes, property locations, healthcare facilities, and everything else is visual noise.
Custom styling lets you suppress what you don't need, emphasize what you do, and ensure the map reads as part of your product rather than a third-party widget dropped in.
Understanding the Style Specification
A MapAtlas style is a JSON document with five key sections:
{
"version": 8,
"name": "My Brand Style",
"sources": { ... },
"glyphs": "https://tiles.mapatlas.eu/fonts/{fontstack}/{range}.pbf?key=YOUR_API_KEY",
"sprite": "https://tiles.mapatlas.eu/sprites/basic?key=YOUR_API_KEY",
"layers": [ ... ]
}
sources, where the map data comes from (MapAtlas vector tile endpoints)glyphs, where to load font files for label renderingsprite, where to load icon images for POI markerslayers, the ordered list of visual layers, each with paint and layout properties
The layers array is where you spend most of your time. Each layer specifies a data type (fill, line, symbol, circle, fill-extrusion), which source features to draw, and how to paint them.
Here's a minimal but complete style that renders a vector tile base:
{
"version": 8,
"name": "Brand Base",
"sources": {
"mapatlas": {
"type": "vector",
"tiles": ["https://tiles.mapatlas.eu/v1/tiles/{z}/{x}/{y}.mvt?key=YOUR_API_KEY"],
"minzoom": 0,
"maxzoom": 14
}
},
"glyphs": "https://tiles.mapatlas.eu/fonts/{fontstack}/{range}.pbf?key=YOUR_API_KEY",
"sprite": "https://tiles.mapatlas.eu/sprites/basic?key=YOUR_API_KEY",
"layers": [
{
"id": "background",
"type": "background",
"paint": { "background-color": "#f5f0eb" }
}
]
}
This renders a blank canvas in your chosen background color. From here you add layers for water, landuse, roads, buildings, and labels.

[Image: Diagram showing the layer stack of a custom map style: background → water → landuse → roads → buildings → labels, with a JSON snippet for each and the resulting visual contribution to the map canvas.]
Dark Mode: Changing the Core Colors
A dark mode map is not simply an inverted raster image. It's a completely different set of paint properties applied to the same vector geometry. This is one of the core reasons vector tiles exist, one tile, infinite visual interpretations.
The key colors to update for a dark theme:
| Layer | Light mode | Dark mode |
|---|---|---|
| Background (land) | #f5f0eb | #1a1a2e |
| Water | #a8d5e5 | #0d3b66 |
| Urban landuse | #e8e0d5 | #16213e |
| Parks/greenspace | #c8e6c9 | #1b4332 |
| Roads (major) | #ffffff | #3a3a5c |
| Roads (minor) | #efefef | #2d2d4a |
| Road labels | #333333 | #cccccc |
Here's how to apply this in a style layer:
{
"id": "water",
"type": "fill",
"source": "mapatlas",
"source-layer": "water",
"paint": {
"fill-color": "#0d3b66",
"fill-opacity": 1
}
}
{
"id": "road-major",
"type": "line",
"source": "mapatlas",
"source-layer": "transportation",
"filter": ["==", "class", "primary"],
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#3a3a5c",
"line-width": ["interpolate", ["linear"], ["zoom"], 10, 1, 16, 4]
}
}
The interpolate expression in line-width scales the road width based on zoom level, narrow at low zoom, wider as you zoom in. This is data-driven styling without any additional network requests.
Applying Brand Colors
Most design systems define a primary color (say, #97C70A), a secondary color, and a neutral palette. The map should use these same values, not approximate them.
Target these layers for brand color application:
Roads as brand elements. If your brand color is vibrant enough, use it for major roads or highlighted routes:
{
"id": "road-highlighted",
"type": "line",
"source": "mapatlas",
"source-layer": "transportation",
"filter": ["==", "class", "primary"],
"paint": {
"line-color": "#97C70A",
"line-width": 3,
"line-opacity": 0.8
}
}
Custom POI markers. Replace the default sprite icons with your own by hosting a custom sprite sheet and updating the sprite URL. Then reference your icons in symbol layers:
{
"id": "brand-poi",
"type": "symbol",
"source": "mapatlas",
"source-layer": "poi",
"layout": {
"icon-image": "brand-pin",
"icon-size": 1.2,
"text-field": ["get", "name"],
"text-font": ["Inter Bold", "Noto Sans Regular"],
"text-size": 12,
"text-offset": [0, 1.5],
"text-anchor": "top"
},
"paint": {
"text-color": "#97C70A",
"text-halo-color": "#1a1a2e",
"text-halo-width": 1
}
}
Custom Fonts for Map Labels
The default map label fonts are generic. If your brand uses Inter, Roboto, or a custom typeface, you can use it for map labels too.
MapAtlas serves glyphs from its own glyph endpoint, which supports the standard Mapbox glyph format. To use a custom font:
- Generate
.pbfglyph files from your typeface using a tool likefontnik. - Host the glyph files on a CDN or your own server.
- Update the
glyphsfield in your style JSON:
{
"glyphs": "https://your-cdn.com/fonts/{fontstack}/{range}.pbf"
}
- Reference the font in any symbol layer's
text-fontproperty:
"text-font": ["Inter Bold", "Noto Sans Regular"]
Always provide a fallback font (Noto Sans covers Unicode characters your primary font may not include). The renderer uses the first font in the array that contains a glyph for each character.
Removing Watermarks and Attribution
MapAtlas allows full white-label deployment. You can remove the MapAtlas logo from the map entirely.
In the SDK, attribution is handled by a control you can disable:
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 12,
attributionControl: false, // Remove the default attribution control
});
If you want to keep attribution but style it to match your UI, hide the default control and add your own:
map.addControl(
new mapmetricsgl.AttributionControl({
customAttribution: 'Map data © OpenStreetMap contributors',
compact: true,
}),
'bottom-left'
);
This replaces the branding with a minimal, legally compliant attribution that respects OpenStreetMap's license while matching your UI. Unlike Google Maps, which requires its logo to remain visible at all times regardless of your plan, MapAtlas gives you full control.

[Image: Before/after comparison. Left: default MapAtlas light style with standard colors and watermark. Right: custom dark style with brand-green roads, deep navy background, dark water, and no watermark, rendered in a dashboard UI context.]
Light/Dark Theme Toggle in React
Here's a complete React component that manages a map with a light/dark toggle, using CSS custom properties to keep the theme in sync with the rest of your UI:
import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const STYLES = {
light: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
dark: 'https://tiles.mapatlas.eu/styles/dark/style.json?key=YOUR_API_KEY',
};
export function BrandedMap({ center = [4.9041, 52.3676], zoom = 12 }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const [theme, setTheme] = useState('light');
useEffect(() => {
const map = new mapmetricsgl.Map({
container: containerRef.current,
style: STYLES.light,
center,
zoom,
attributionControl: false,
});
map.addControl(
new mapmetricsgl.AttributionControl({ compact: true }),
'bottom-left'
);
mapRef.current = map;
return () => map.remove();
}, []);
useEffect(() => {
if (mapRef.current) {
mapRef.current.setStyle(STYLES[theme]);
}
}, [theme]);
return (
<div style={{ position: 'relative' }}>
<div ref={containerRef} style={{ width: '100%', height: '500px' }} />
<button
onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
style={{
position: 'absolute',
top: 12,
right: 12,
padding: '8px 16px',
background: theme === 'dark' ? '#ffffff' : '#1a1a2e',
color: theme === 'dark' ? '#1a1a2e' : '#ffffff',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
zIndex: 1,
}}
>
{theme === 'light' ? 'Dark mode' : 'Light mode'}
</button>
</div>
);
}
The setStyle call re-renders the map with the new color scheme. Tile data that's already in cache is not re-fetched, only the visual interpretation changes.
Hosting a Custom Style JSON
For full control, host your style JSON as a static file on your CDN or S3 bucket. This lets you:
- Version-control your style alongside your application code.
- Update the visual design without redeploying the application (just update the hosted JSON).
- Use different styles for different environments (preview vs. production).
Load a hosted style at runtime:
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://your-cdn.com/styles/brand-dark.json',
center: [4.9041, 52.3676],
zoom: 12,
});
The only constraint is CORS: the style JSON must be served with Access-Control-Allow-Origin: * (or your specific domain) so the browser can fetch it cross-origin.
Style Development Workflow
The fastest way to build a custom style is iterative, see changes in real time without rewriting code.
- Start with a MapAtlas base style JSON as your foundation. Download it by fetching
https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY. - Edit the JSON in your editor, watching which layer affects which visual element.
- Reload the map with your modified style JSON to see the result.
- Once stable, commit the style JSON to your repository and host it on your CDN.
For deeper customization, check the Map Visualization & Styling documentation for the full layer reference, paint property types, and expression syntax.
If you're coming from Mapbox and wondering what changed, see Mapbox vs. MapAtlas: Which Maps API Is Right for Your EU Project? for a side-by-side comparison of features and pricing.
And for the architectural underpinning of why vector tiles make all of this possible, Vector Tiles vs. Raster Tiles explains the rendering pipeline in detail.
Summary
Custom map styling is not a luxury, it's the difference between a map that feels like a third-party widget and one that feels like it was built for your product. With MapAtlas:
- The style specification is standard JSON compatible with Mapbox GL JS tooling.
- Dark mode is a style JSON swap, not a new tile set or server configuration.
- Brand colors apply to roads, labels, and POI icons through paint properties.
- Custom fonts load from any glyph endpoint you control.
- Watermarks and attribution are fully configurable, white-label is supported on all paid plans.
- The entire style can be toggled at runtime without page reload.
Start building your branded map today. Sign up for a free MapAtlas API key and load the base style JSON as your starting point. The first custom style takes about 30 minutes to get right, subsequent iterations take minutes.
