When building route planners, logistics dashboards, or public transport maps, you often need to display multiple routes that share the same roads. Without proper handling, these routes stack on top of each other and only the last-drawn route is visible. Users cannot compare routes or even confirm that all paths are displayed.
In a previous tutorial, we explored three approaches to solve this problem in Leaflet. MapLibre GL offers a simpler solution: the built-in line-offset paint property. No plugins, no geometry manipulation - just a single property that visually shifts each route by a specified number of pixels.
This tutorial shows how to use line-offset to display multiple overlapping routes clearly on a MapLibre GL map.
Try the live demo:
View on CodePen
APIs used:
- Routing API - calculate routes between waypoints
- Map Tiles API - vector map style
- Map Marker Icon API - custom markers
What you will learn:
- When overlapping routes are a problem and how to solve it
- How to use MapLibre GL's
line-offsetproperty - Understanding the line layer paint parameters
- Creating custom markers with the Map Marker Icon API
Table of Contents
- When Overlapping Routes Matter
- The Solution: line-offset Property
- Set Up Routes and Offsets
- Add Route Layers with Offset
- Understanding the Paint Parameters
- Add Custom Markers
- Explore the Demo
- Summary
- FAQ
When Overlapping Routes Matter
Multiple routes sharing the same roads is common in several scenarios:
Route Planners
When comparing alternative routes from A to B, the paths often overlap near the origin and destination. Users need to see all options to make informed decisions about travel time, distance, or toll roads.
Delivery and Logistics
Multiple drivers heading to the same warehouse or distribution center converge on the same streets. Dispatchers need to see all routes simultaneously to coordinate timing and identify bottlenecks.
Public Transport Maps
Bus or train lines frequently share corridors. A transit map that only shows one line per street fails to communicate the actual service coverage.
Multi-stop Route Comparison
When planning trips with multiple waypoints, users might want to compare different orderings. The routes will share many segments but differ in key sections.
In all these cases, stacked routes create confusion. The map appears to show fewer routes than expected, and users cannot visually compare the paths.
Four routes from different Paris landmarks to the Arc de Triomphe. Each route is visible even where they share the same streets.
The Solution: line-offset Property
MapLibre GL provides a built-in solution: the line-offset paint property. This property shifts the rendered line perpendicular to its direction by a specified number of pixels.
paint: {
"line-color": "#E53935",
"line-width": 4,
"line-offset": -6 // Shift 6 pixels to the left
}
Key characteristics of line-offset:
- Pixel-based: The offset is in screen pixels, not meters. Routes maintain consistent visual separation at all zoom levels.
- Perpendicular shift: The line moves sideways relative to its direction, creating parallel paths.
- No geometry modification: The underlying coordinates remain unchanged. Only the visual rendering is affected.
- Positive/negative values: Positive values shift right (relative to line direction), negative values shift left.
For four routes, we use offsets like [-6, -2, 2, 6] to spread them evenly across a 12-pixel width.
This is much simpler than the Leaflet approaches we covered previously. In Leaflet, you need external libraries like Turf.js for geometric offset or plugins like leaflet-polylineoffset. MapLibre GL handles it natively.
Set Up Routes and Offsets
Define your routes with different colors and assign pixel offsets to each:
const DESTINATION = {
lat: 48.8738,
lon: 2.2950,
name: "Arc de Triomphe"
};
const ROUTES = [
{id: "r1", name: "From Eiffel Tower", color: "#E53935", origin: {lat: 48.8584, lon: 2.2945}},
{id: "r2", name: "From Louvre", color: "#43A047", origin: {lat: 48.8606, lon: 2.3376}},
{id: "r3", name: "From Notre-Dame", color: "#1E88E5", origin: {lat: 48.8530, lon: 2.3499}},
{id: "r4", name: "From Sacré-Cœur", color: "#FB8C00", origin: {lat: 48.8867, lon: 2.3431}}
];
// Offset in pixels for parallel routes (visual offset only)
const OFFSETS = [-6, -2, 2, 6];
The offset values spread routes symmetrically: two routes shift left (-6, -2 pixels) and two shift right (2, 6 pixels). This keeps the routes centered on the actual road.
API Key: Sign up at geoapify.com to get your API key.
Add Route Layers with Offset
Each route needs two layers: an outline (darker, wider) and the main line (route color, narrower). The outline creates visual depth and helps routes stand out from the map.
function addRouteLayer(routeId) {
const route = routeState[routeId];
const offsetPixels = OFFSETS[route.index % OFFSETS.length];
// Add source with original route geometry (no geometric offsetting)
map.addSource(routeId, {
type: "geojson",
data: route.feature
});
// Add outline layer with pixel-based visual offset
map.addLayer({
id: `${routeId}-outline`,
type: "line",
source: routeId,
layout: {
"line-join": "round",
"line-cap": "round"
},
paint: {
"line-color": darkenColor(route.color, 30),
"line-width": 7,
"line-opacity": 0.9,
"line-offset": offsetPixels
}
});
// Add main line layer with same pixel-based visual offset
map.addLayer({
id: `${routeId}-line`,
type: "line",
source: routeId,
layout: {
"line-join": "round",
"line-cap": "round"
},
paint: {
"line-color": route.color,
"line-width": 4,
"line-opacity": 1,
"line-offset": offsetPixels
}
});
}
Both layers use the same line-offset value so the outline and main line move together. The outline is 7 pixels wide, the main line is 4 pixels, creating a 1.5-pixel border on each side.
Understanding the Paint Parameters
The line layer paint properties control how routes appear on the map. Here's what each parameter does:
line-offset
"line-offset": offsetPixels // e.g., -6, -2, 2, or 6
The core parameter for this tutorial. It shifts the line perpendicular to its direction:
- Negative values shift the line to the left (relative to the line direction)
- Positive values shift the line to the right
- Zero draws the line on the actual coordinates
For multiple routes, distribute offsets evenly. With 4 routes using [-6, -2, 2, 6], routes are spaced 4 pixels apart, creating a total width of 12 pixels.
line-width
"line-width": 7 // outline
"line-width": 4 // main line
The thickness of the line in pixels. The outline is wider than the main line to create a border effect. The difference (7 - 4 = 3 pixels) is split between both sides, giving a 1.5-pixel border.
line-color
"line-color": darkenColor(route.color, 30) // outline - 30% darker
"line-color": route.color // main line - original color
The outline uses a darkened version of the route color. This creates depth and helps distinguish overlapping routes. The darkenColor helper reduces RGB values by a percentage.
line-opacity
"line-opacity": 0.9 // outline - slightly transparent
"line-opacity": 1 // main line - fully opaque
Opacity ranges from 0 (invisible) to 1 (fully opaque). The outline is slightly transparent (0.9) to soften the border effect.
Layout properties: line-join and line-cap
layout: {
"line-join": "round",
"line-cap": "round"
}
-
line-join: How line segments connect at corners.
"round"creates smooth curves at turns. Other options:"miter"(sharp corners),"bevel"(flat corners). -
line-cap: How line ends are drawn.
"round"adds a semicircle at each end. Other options:"butt"(flat end),"square"(flat end extended by half the line width).
Using "round" for both creates smooth, professional-looking routes.
The line-offset property shifts each route perpendicular to its direction, creating clear visual separation.
Add Custom Markers
Use the Geoapify Map Marker Icon API to create markers for origins and the destination. Each origin marker matches its route color, making it easy to identify which path starts where.
function createMarker(lon, lat, color, iconName, name, label) {
const el = document.createElement("div");
el.style.width = "36px";
el.style.height = "48px";
el.style.backgroundImage = `url(https://api.geoapify.com/v2/icon?type=awesome&color=${color}&icon=${iconName}&iconType=awesome&size=48&scaleFactor=2&apiKey=${API_KEY})`;
el.style.backgroundSize = "contain";
el.style.backgroundRepeat = "no-repeat";
el.style.cursor = "pointer";
const popup = new maplibregl.Popup({offset: [0, -48]})
.setHTML(`<strong>${name}</strong><br>${label}`);
const marker = new maplibregl.Marker({element: el, anchor: "bottom"})
.setLngLat([lon, lat])
.setPopup(popup)
.addTo(map);
return marker;
}
Map Marker Icon API Parameters
The marker URL includes several parameters to customize the icon. See the full Marker Icon API documentation for all options.
| Parameter | Value | Description |
|---|---|---|
type |
awesome |
Marker style. Options: material (Material Design shape), awesome (Font Awesome marker shape), circle, plain (no background) |
color |
%23E53935 |
Background color of the marker in hex (URL-encoded # as %23) |
icon |
flag, circle
|
Name of the inner icon. Depending on iconType, use Material Design icon names or Font Awesome v6 icon names
|
iconType |
awesome |
Icon library: material or awesome. Default depends on marker type |
size |
48 |
Icon image height in pixels |
scaleFactor |
2 |
Multiplier for retina displays (2x resolution) |
The destination uses a flag icon, while origins use circle icons with colors matching their routes.
MapLibre GL Marker Options
new maplibregl.Marker({element: el, anchor: "bottom"})
- element: Custom HTML element to use as the marker (instead of default pin)
-
anchor: Which part of the element sits on the coordinates.
"bottom"places the pin tip on the location
The popup offset [0, -48] positions it above the marker (negative Y moves up).
Explore the Demo
The interactive demo shows four routes from Paris landmarks to the Arc de Triomphe. You can:
- Toggle routes using checkboxes to show/hide individual paths
- Click a route in the list to zoom and see details
- Compare visually how routes overlap and diverge
Notice how the line-offset property keeps all routes visible even on shared road segments. Each color remains distinct without any complex geometry manipulation.
Summary
MapLibre GL's line-offset property provides a simple, built-in solution for displaying multiple overlapping routes. Unlike Leaflet, which requires external libraries or plugins, MapLibre GL handles parallel route visualization natively.
Key points:
- line-offset is pixel-based - Routes maintain consistent visual separation at all zoom levels
- No geometry modification - The underlying coordinates stay unchanged; only rendering is affected
-
Simple implementation - Just add
"line-offset": pixelValueto your paint properties - Works with outlines - Apply the same offset to both outline and main line layers
Recommended offset values:
- For 2-3 routes:
[-4, 0, 4]or[-3, 3] - For 4 routes:
[-6, -2, 2, 6] - For more routes: Spread evenly, keeping total width under 20 pixels
When to use this approach:
- Comparing alternative routes between the same points
- Displaying multiple delivery or logistics routes
- Building public transport maps with shared corridors
- Any scenario where users need to see all routes simultaneously
Useful links:
- Geoapify Routing API
- MapLibre GL line-offset documentation
- Map Marker Icon API
- Routing API Playground
FAQ
Q: How does line-offset compare to Turf.js lineOffset?
A: MapLibre GL's line-offset is a visual/pixel-based offset applied during rendering. Turf.js lineOffset modifies the actual geometry in meters. The MapLibre approach is simpler and maintains consistent screen-space separation at all zoom levels. Turf.js creates truly parallel paths in geographic space, which may be better for precise geographic analysis.
Q: What happens at sharp turns or intersections?
A: The line-offset property handles corners reasonably well due to the line-join: round setting. At very sharp turns, you might see slight visual artifacts, but for typical road networks this is rarely noticeable.
Q: Can I change the offset dynamically?
A: Yes. Use map.setPaintProperty(layerId, 'line-offset', newValue) to update the offset without removing and re-adding layers. This is useful for interactive applications where users might adjust the separation.
Q: How many routes can I display before performance suffers?
A: MapLibre GL uses GPU rendering and handles dozens of routes efficiently. The line-offset calculation is done on the GPU, so it adds minimal overhead. For hundreds of routes, consider server-side simplification or clustering.
Q: Can I use different offsets at different zoom levels?
A: Yes. MapLibre GL expressions support zoom-based interpolation. For example:
"line-offset": [
"interpolate", ["linear"], ["zoom"],
10, 2, // 2 pixels at zoom 10
16, 8 // 8 pixels at zoom 16
]
Q: What marker icon types are available?
A: The Map Marker Icon API supports four types: material (Material Design shape), awesome (Font Awesome marker shape), circle (simple circle), and plain (no background shape). Each can include an icon from Font Awesome v6 or Material Design Icons libraries.
Try It Now
Sign up at geoapify.com to get your API key and start building multi-route visualizations with MapLibre GL.


Top comments (0)