Make your travel website more attractive and intuitive by displaying the destination as an itinerary on a map instead of showing the itinerary in text or timeline format. This becomes an easy task with MB Views from Meta Box in this guide.
I have a single page for a tour as an example:

On the map, we’ll have these functionalities:
- Each destination in the tour is marked with a number on the map. They are connected by lines.
- Quickly see the distance between stops.
- When you click on a specific day, the map automatically centers to the corresponding destination, along with its details.
We also suggest the two destination tabs, on the sidebar, or the content area.
Displaying destinations by itinerary on the map is useful in many cases:
- Travel blogs and tour websites: provides a clear overview of the entire trip, helping customers explore each stop directly on the map before booking.
- Hotels and resorts: showcase nearby attractions, giving guests a better sense of what's around the property.
- Food tour: recommend the best local eateries along the route.
…
Before building it step by step, let’s check some tools we need for this practice.
Video Version
Before Getting Started
Each tour is a post of the custom post type. And the detailed information will be saved in custom fields. In there, each destination of days in the itinerary is an item of a cloneable group. After having all the needed content, I use MB Views to display it in the itinerary format on the map.
So, I highly recommend you use Meta Box AIO to have both the framework and advanced features from all the Meta Box extensions. These are extensions we use:
- MB Custom Post Type: to create a custom post type for tours.
- MB Builder: to have UI in the backend to create custom fields visually.
- MB Group: to organize custom fields into groups for easier management.
- MB Views: to create a view to display data in the itinerary.
Now, let's dive in.
Create Custom Post Type and Custom Fields
We have some tutorials that focus on creating a custom post type and custom fields. So, I skipped it to save time; you can check them for more details. I’ve already created the tour post type and a field group for it.

The group is for the destination is cloneable since there is more than one stop in a tour.
When you create a new tour, you can see the created fields. Fill in data for them.
Display Data on the Map
In this step, we’ll get data and display it on the map on the frontend. Transforming it to itinerary will be done later.
Get Data
Go to Meta Box > Views, and create a new view.
In the Template tab, to get and display data, you can add code directly or use the Insert Field button, then pick the field you want to insert from the list.

After inserting all the fields you want to get data from, move to the Settings section to set the Type of the view as Singular, and choose Tour.

Then, when you view the post on the frontend, you can see all the data is displayed but in text and image format only.
Display Destinations on Map and Tabs
I’ll update the view to style the destination in tabs, and add a map to the single page, by adding some code.

In there:
First, I load the icon for the total duration of the tour, and declare that we’ll use the JavaScript library for the map.
To calculate the duration, I add:
{% set total_stops = post.schedule|length %}
{% set nights = total_stops - 1 %}
The number of days is determined by the number of items in the schedule field. We then subtract one to calculate the number of nights.
I’m adding some div tags and classes, as well as modifying the code for styling later. I also tweak the price format a bit to make it easier to read.
I want to display the days as tabs, so I add this part:
<div class="day-tabs">
<button class="day-tab active" data-day="all">All</button>
{% for clone in post.schedule %}
<button class="day-tab" data-day="{{ loop.index }}">Day {{ loop.index }}</button>
{% endfor %}
</div>
It includes the "All" tab, allowing visitors to view all destinations at once, and tabs for days. Then, I loop through the schedule field. For each item, I generate a button labeled "Day 1", "Day 2", and so on. I use the data-day attribute to store the day number, which we'll use later to filter the corresponding destinations on the map.
I also show the tabs on the sidebar. I loop through the schedule field again to generate a card for each stop in the sidebar. Each card stores its index, day number, latitude, and longitude as data attributes. We'll use these values later to synchronize the sidebar with the map and display the correct location when a visitor interacts with it.
Next, I pass all the itinerary data from the schedule field into a JavaScript array. Similar to the card on the sidebar, each stop includes some types of data, so we can use it later to build the interactive map.
Then, navigate to the CSS tab to add some CSS for styling.

Now, to add the map and handle the action on it, we’ll move to the JavaScript tab to add some code.

Specifically:
if (!window.tourStops || !window.tourStops.length) return; const stops = window.tourStops; let map
These lines to validate and prepare the data. I check whether the tourStops array exists and contains any data. If it's empty, the script simply stops to prevent JavaScript errors. Then, assign that array to a local variable. This makes the rest of the code cleaner and easier to read.
We’ll add the app and use functionalities on the map via the Leaflet library. So I use this part to load it:
const script = document.createElement('script');
script.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
script.onload = initMap;
document.head.appendChild(script);
Instead of including Leaflet directly in the page, I create an element dynamically. The code ensures that the map is initialized only after the Leaflet library has finished loading. While append the script element to the document's <head>, which starts downloading the library.
Now, let’s initialize the map.
First, I create a Leaflet map inside the tour-map element with the initial center of the map using the latitude and longitude of the first stop. You can change this number to regulate the initial zoom level.
The next part is to add a tile layer from OpenStreetMap. It’s used to display roads, cities, and geographical information on the map. Then fit the map so that every destination can be visible.
We the new look of my page with a map, and destination tabs in two places.
But the tabs haven’t worked yet; the itinerary hasn’t displayed as well.
So, let’s move to the next step.
Add Interactivity to the Map
We also use JavaScript in this step. Back to the JavaScript tab that we just used, then add the code as below:

Let’s break down the code in detail.
First, I declare a few variables that we'll use throughout the script.
Then, to create markers and set them interactive, I add this part.
stops.forEach(function (stop, i) {
const marker = L.marker([stop.lat, stop.lng], {
icon: makeIcon(stop.index, false)
}).addTo(map);
marker.bindPopup(popupHtml(stop), { maxWidth: 220 });
marker.on('click', function () { setActiveCard(i); });
markers.push(marker);
});
I loop through the stops array and create a marker for each stop on the map. For each stop, I create a Leaflet marker using its latitude and longitude. I also assign a custom numbered icon and add the marker to the map. Then attach a popup to the marker that displays information about the selected stop. When a visitor clicks a marker, the corresponding card in the sidebar is automatically highlighted. This keeps the map and sidebar synchronized.
function makeIcon(index, dimmed) {
return L.divIcon({
className: '',
html: '<div class="map-marker' + (dimmed ? ' dimmed' : '') + '">' + index + '</div>',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -20]
});
}
Next, this is to customize the markers. Unlike the default Leaflet marker, this lets me build the marker with HTML and CSS. The marker displays the stop number, supports a dimmed state for filtering, and defines its size, anchor point, and popup position for proper alignment.
function popupHtml(stop) {
const img = stop.image ? '<img src="' + stop.image + '" style="...">' : '';
return '<div ...>' + img + '<div>Day ' + stop.day + '</div>' + '<div>' + stop.name + '</div>' + '</div>';
}
When you click on a marker, this will be generate the popup content. It includes an image, the day number, and the stop name. Then, these lines are to keep the sidebar synchronized with the map.
function setActiveCard(index) {
$('.stop-card').removeClass('active');
const $card = $('.stop-card[data-index="' + index + '"]').addClass('active');
if ($card.length) {
const $sidebar = $('.tour-sidebar');
$sidebar.animate({ scrollTop: $card.position().top + $sidebar.scrollTop() - 20 }, 300);
}
}
Whenever a stop is selected, it highlights the corresponding card and automatically scrolls the sidebar to bring that card into view.
Next, I create a helper function to automatically fit the map to the current list of stops.
function fitBounds(list) {
if (!list.length) return;
map.fitBounds(
L.latLngBounds(list.map(function (s) { return [s.lat, s.lng]; })),
{ padding: [40, 40] }
);
}
It converts each stop into a pair of coordinates, calculates the optimal map bounds, and adds a small amount of padding so all markers remain comfortably visible.
I want to show the distance between two stops, so I use the Haversine formula to calculate it.
Also, I use the clearPolyline() function to clean up the old polyline and distance to avoid displaying multiple routes at the same time.
Next, the drawAnimatedPolyline() function draws an animated route connecting all the stops on the map. You need to check whether there are at least two stops. The distance label will be displayed in the midpoint of the route.
Next, this part is to animate the route.
function drawStep() {
if (pointIndex >= allPoints.length) return;
const current = allPoints[pointIndex];
path.push(current.point);
polyline.setLatLngs(path);
const next = allPoints[pointIndex + 1];
const segmentDone = next && next.segmentIndex !== current.segmentIndex;
if (segmentDone && distanceLabels[current.segmentIndex]) {
distanceLabels[current.segmentIndex].setOpacity(1);
}
pointIndex++;
setTimeout(drawStep, STEP_MS);
}
Each point is added to the current path, and the polyline is updated immediately. This creates the effect of the route being drawn progressively. Then, I check whether the current route segment has been completed. Once a segment is finished, I reveal its distance label. So, as the route is drawn, the distance between each pair of stops is revealed one segment at a time, making the itinerary easier to follow and more visually appealing.
Now, I want the interaction to work in the opposite direction, allowing the tabs to control the map as well.
function filterByDay(day) {
const isAll = day === 'all';
const list = isAll ? stops : stops.filter(function (s) { return s.day == day; });
$('.stop-card').each(function () {
const match = isAll || $(this).data('day') == day;
$(this).toggleClass('hidden', !match).toggleClass('show-desc', !isAll);
});
stops.forEach(function (s, i) {
markers[i].setIcon(makeIcon(s.index, !(isAll || s.day == day)));
});
setTimeout(function () { drawAnimatedPolyline(list); }, 2000);
fitBounds(list);
const first = stops.findIndex(function (s) { return isAll || s.day == day; });
if (first !== -1) setActiveCard(first);
}
I add this part to handle the action when the customer selects a day. If the all tab is selected, all the destinations are shown; otherwise, the chosen stop is displayed on the map. Then, 4 actions will be run at the same time: show or hide the stop card on the sidebar along with its description; update the marker on the map, the markers that don't belong to the selected day are displayed in a dimmed state; redraw the route using only the filtered stops, and adjusts the map view, the first matching stop is highlighted to provide a seamless user experience.
$('.stop-card').on('click', function () {
const index = parseInt($(this).data('index'));
map.flyTo([stops[index].lat, stops[index].lng], 13, { duration: 0.8 });
markers[index].openPopup();
setActiveCard(index);
});
This part is to synchronize the sidebar with the map. When a visitor clicks a stop card, I retrieve the index of the selected stop from this attribute, then the map smoothly flies to that location, opens the corresponding popup, and highlights the selected card.
And finally, I handle clicks on the day tabs.
$('.day-tab').on('click', function () {
$('.day-tab').removeClass('active');
$(this).addClass('active');
filterByDay($(this).data('day'));
});
Whenever a visitor selects a different day, the itinerary is updated accordingly. Specifically, remove the active class from all tabs to clear the previous selection and add that class to the clicked tab, making it visually clear which day is currently selected. Then, I retrieve the selected day from this attribute and pass it to this function. It takes care of updating the sidebar, markers, animated route, and map view to display only the stops for the selected day. That’s all.
I uploaded the code to GitHub, so you can refer to it.
And now, let’s see the final result. The itinerary works smoothly, and we have a bi-directional interaction between the tabs and the map.
Last Words
With MB Views, you can add a destination itinerary on the map easily. And your site will be attractive to visitors. Try it and let us know your thoughts.
We have some other tutorials on interacting with the map; you can refer to them on your website.
- How To Display All Listings On A Map With Meta Box
- Insert Google Maps to a Website using Custom Fields
- How to Search Posts on a Map - Using MB Views
- How to Search Nearby Shops and Get Directions on a Map Listing - Using MB Views
- How to Display a Travel Itinerary on the Map - Using MB Views
- Advanced Map Listings
- Author Bio
- Better 404 Page
- Blogs for Developers
- Building a Simple Listing Website with Filters
- Building an Event Website
- Building Forms with MB Frontend Submission
- Coding
- Create a Chronological Timeline
- Custom Fields Fundamentals
- Design Patterns
- Displaying Posts with Filters
- Download and Preview Buttons
- Dynamic Banners
- Dynamic Landing Page
- FAQs Page
- Featured Products
- Filter Posts by Relationships
- Filter Posts by Taxonomy
- Full Site Editing
- Google Fonts
- Gutenberg
- Hotel Booking
- Latest Products
- Logo Carousel
- MB Builder Applications
- MB Group Applications
- MB Views Applications
- Most Viewed Posts
- Opening Hours
- OTA Website
- Pricing Table Page
- Product Page
- Product Variations
- Querying and Showing Posts by Custom Fields
- Recipe
- Related Posts via Relationship
- Restaurant Menus
- SEO Analysis
- Simple LMS
- Speed Up Website
- Taxonomy Thumbnails
- Team Members
- User Profile
- Video Gallery
How to Search Nearby Shops and Get Directions on a Map Listing - Using MB Views
How to Pin Newest Featured Posts Above Regular Posts Using MB Views
Insert Google Maps to a Website using Custom Fields