Displaying a route on a map is straightforward. Making it look good takes more thought. A plain blue line works, but professional applications use shadows, custom colors, and polished markers to create routes that stand out and communicate clearly.
Route styling impacts both aesthetics and usability. The right combination of line weight, shadows, and marker design helps users quickly understand the path while keeping the underlying map readable.
This tutorial walks through building a route visualization with interactive styling controls. You will see how each parameter affects the result and learn the techniques to create professional-looking routes in your own applications.
APIs used:
- Routing API - calculate route between waypoints
- Map Tiles API - base map layer
- Map Marker Icon API - custom waypoint markers
What you will learn:
- How to render route GeoJSON on a Leaflet map
- Techniques for adding shadows and outlines to route lines
- Using custom panes for proper layer ordering
- Creating markers with the Map Marker Icon API
- Building interactive controls to experiment with styling
Table of Contents
- The Goal: Professional Route Styling
- Set Up the Map
- Fetch the Route
- Render the Route with Styling
- Add Route Shadows
- Create Custom Markers
- Build Interactive Controls
- Explore the Demo
- Summary
- FAQ
The Goal: Professional Route Styling
A well-styled route has several characteristics:
- Visible at all zoom levels: Not too thin to see, not too thick to obscure the map
- Clear boundaries: A shadow or outline separates the route from the map
- Consistent appearance: Rounded line caps and joins for smooth corners
- Informative markers: Numbered waypoints that match the route style
The demo provides controls to adjust all these parameters. This lets you experiment and find the right balance for your use case.
Set Up the Map
Initialize a Leaflet map and create custom panes for layer ordering. Panes control the z-index, ensuring the shadow appears below the route line.
const map = L.map("map", {zoomControl: false}).setView([48.8606, 2.3376], 13);
L.control.zoom({position: "bottomright"}).addTo(map);
// Custom panes for proper z-index layering (shadow below route)
map.createPane("route-shadow");
map.getPane("route-shadow").style.zIndex = 399;
map.createPane("route-line");
map.getPane("route-line").style.zIndex = 400;
// Geoapify map tiles
L.tileLayer(`https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}@2x.png?apiKey=${API_KEY}`, {
attribution: '© <a href="https://www.geoapify.com/">Geoapify</a> © OpenMapTiles © OpenStreetMap',
maxZoom: 20
}).addTo(map);
Without custom panes, the shadow might appear above the route depending on the order layers are added.
Key: Sign up at geoapify.com to get your API key.
Fetch the Route
The Routing API accepts waypoints as lat,lon pairs separated by |. It returns GeoJSON that Leaflet can render directly.
// Build routing URL (waypoint format: lat,lon separated by |)
const routingUrl = `https://api.geoapify.com/v1/routing?waypoints=${waypoints.map(w => `${w.lat},${w.lon}`).join("|")}&mode=drive&apiKey=${API_KEY}`;
function fetchRoute() {
fetch(routingUrl)
.then(res => res.json())
.then(data => {
if (data.features?.length > 0) {
routeFeature = data.features[0];
renderRoute(routeFeature);
renderMarkers();
fitBounds();
}
});
}
We store the route feature in routeFeature so we can re-render it when styling changes without fetching again.
Render the Route with Styling
Render the route using L.geoJSON() with styling options:
routeLayer = L.geoJSON(feature, {
pane: "route-line",
style: {
color: routeColor,
weight: routeWidth,
opacity: routeOpacity,
lineCap: "round",
lineJoin: "round"
}
}).addTo(map);
Key styling properties:
- color: Line color (hex value)
- weight: Line thickness in pixels
- opacity: Transparency (0-1)
- lineCap/lineJoin: Use "round" for smooth ends and corners
Here's how the route looks with and without shadow applied:
Add Route Shadows
Shadows create depth and help the route stand out. Draw a wider, darker line behind the main route:
shadowLayer = L.geoJSON(feature, {
pane: "route-shadow",
style: {
color: shadowColor,
weight: routeWidth + 4,
opacity: shadowOpacity,
lineCap: "round",
lineJoin: "round"
}
}).addTo(map);
The shadow is 4 pixels wider than the route (2 pixels on each side). Using a separate pane guarantees it renders below the main route.
Tip: Semi-transparent black works well on most map styles. For dark maps, try a lighter shadow color.
Create Custom Markers
The Map Marker Icon API generates marker images on the fly. Create numbered markers with custom colors:
const icon = L.icon({
iconUrl: `https://api.geoapify.com/v2/icon?type=awesome&color=${color}&text=${number}&size=${size}&scaleFactor=2&apiKey=${API_KEY}`,
iconSize: [size * 0.75, size],
iconAnchor: [size * 0.375, size - 3],
popupAnchor: [0, -size + 5]
});
const marker = L.marker([wp.lat, wp.lon], {icon})
.bindPopup(`<strong>${wp.name}</strong><br>Waypoint ${idx + 1}`)
.addTo(map);
Three marker types are available:
- awesome: Classic map pin shape
- material: Google Maps style pin
- circle: Simple circle marker
The scaleFactor=2 parameter provides high-resolution icons for retina displays.
Build Interactive Controls
Each control updates the route or markers in real-time by calling the render functions:
document.getElementById("route-color").addEventListener("input", (e) => {
document.getElementById("route-color-value").textContent = e.target.value.toUpperCase();
if (routeFeature) renderRoute(routeFeature);
});
document.getElementById("marker-type").addEventListener("change", renderMarkers);
The pattern is simple: listen for input changes, update the displayed value, and re-render the affected layer.
Explore the Demo
The interactive demo lets you experiment with all styling parameters in real-time. Try adjusting:
- Route styling - color, width, and opacity
- Shadow effects - toggle, color, and opacity
- Marker types - awesome pins, material pins, or circles
- Marker size - from 32px to 80px
- Shadow toggle - for both routes and markers
Play with different combinations to find what works best for your application. The reset button restores default settings.
👉 Try the live demo:
Summary
Visualizing routes on a Leaflet map involves more than just drawing a line. This tutorial covered:
- Custom panes for controlling layer order (shadow below route)
- Route styling with color, width, opacity, and line caps
- Shadow technique using a wider, darker line behind the main route
- Map Marker Icon API for customizable waypoint markers
- Interactive controls for experimenting with parameters
Recommended settings for most applications:
- Route width: 5-8 pixels
- Shadow: 3-4 pixels wider than route, 0.2-0.4 opacity
- Line caps: "round" for smooth appearance
- Marker size: 40-56 pixels depending on map zoom
Key takeaways:
- Custom panes guarantee consistent layer ordering
- Shadows add depth and improve route visibility
- The Map Marker Icon API provides flexible marker customization
- Rounded line caps and joins create professional-looking routes
- Interactive controls help find the right balance for your use case
Useful links:
- Geoapify Routing API Documentation
- Routing API Playground
- Map Marker Icon API
- Leaflet Path Options
- Leaflet Map Panes
FAQ
Q: What's the difference between shadow and outline?
A: Shadow is a wider line drawn behind the route with lower opacity, creating a soft depth effect. Outline would be a border around the route line. In this tutorial, we use the shadow technique as it provides better visual separation on most map styles.
Q: Why use custom panes instead of z-index?
A: Leaflet's pane system provides explicit control over layer ordering regardless of when layers are added to the map. With z-index, layer order can depend on add order, leading to unpredictable results. Custom panes guarantee the shadow always renders below the route.
Q: How do I choose the right route color?
A: Consider contrast with your map tiles. Blue (#2196F3) works well on most map styles. For dark maps, use brighter colors. Avoid red or orange as they often indicate traffic or warnings. Test at different zoom levels to ensure visibility.
Q: Can I animate the route drawing?
A: Yes, but that requires a different approach. You would progressively reveal the route by creating polylines with increasing coordinate arrays or using CSS animations on SVG paths. The GeoJSON approach shown here is not optimized for animation.
Q: How many waypoints can I display before performance suffers?
A: Leaflet handles dozens of markers well. For the route itself, complexity depends on the number of coordinate points, not waypoints. Routes with thousands of points may benefit from simplification using libraries like Turf.js.
Q: Can I style different segments of the route differently?
A: Yes. Split the route GeoJSON into separate features and render each with different styles. This is useful for highlighting specific segments, showing traffic conditions, or indicating different transportation modes.
Q: How do I make routes clickable?
A: Add event listeners to the route layer. For example: routeLayer.on('click', function(e) { ... }). You can show popups, highlight the route, or trigger other actions.
Try It Now
Sign up at geoapify.com to get your API key and start building beautifully styled routes.



Top comments (0)