Helping guide you through the never-ending forest of technology, into the open glade of easy to follow posts!
Creating an Interactive World Map: With Leaflet.js
Creating an Interactive World Map: With Leaflet.js

Creating an Interactive World Map: With Leaflet.js

I’ve been playing Dungeons and Dragons as a player for quite a while, but when the latest adventure came to a close I decided to step up to the plate and run the next campaign in my own Homebrew setting!

I really wanted to give my players a world map that allowed them to zoom and pan around to explore the world I had created with markers on towns and cities, but couldn’t find anything that fit the build, until I discovered Leaflet.js (which is even mobile-friendly!)

If you’re wanting to see the end results of this project before starting, you can see the map and code at the bottom of this post. This guide will work on both Mac and Windows and whilst the guide does involve typing a little bit of code, everything is detailed below. You’ll also need Photoshop, anything higher than CS2 will work.

Getting Your World Map Ready

For the purposes of this guide, I’m going to assume that you’ve already got a world map ready to work on! If you don’t currently have one then you can generate lovely looking maps on Azgaar’s Fantasy Map Generator.

After finalising your world map you need to convert your single image map into a collection of tiles–this is to save on bandwidth, but also provides tiles for the different zoom levels, as less detail is needed when zoomed out.

The first thing you’ll want to do is create a project folder, this will contain everything needed for the end result to upload!

To create the tiles you’ll need to use the photoshop-google-maps-tile-cutter script by bramus. This script is straightforward but can take a while depending on the size of your map. Other than the export path, you’ll want the script export settings to match the settings below. Additionally, you can change the background colour to match your water.

NOTE: Make sure that your map size is a multiple of 256, just increase your canvas size to the next multiple. (My map is 15360 x 15360)

This script also creates a Google Maps version of the image, however, due to some API changes it no longer works properly–this was the first route I went down before encountering these issues.

After you’ve created all of your tiles you’ll want to run the end results through a .png optimiser. This gets rid of unneeded data without any quality reduction. My program of choice for this is PNGYU which is based on pngquant and allows batch compressing!

To do this, drag the whole exported folder into PNGYU, and use the following settings.

When this has finished exporting, you’ll want to place the end result in its own folder within your project folder. I recommend naming this folder map, the end result should look something like this.

Getting Started With Leaflet.js

Now that you’ve tiled and compressed your map it’s now time to get started with Leaflet.js. The first thing you’ll want to do is jump over to their website, The website is extremely useful and well documented, so if you’re after additional features I recommend having a look!

Navigate to the downloads page and download the latest version (Leaflet 1.5.1 at the time of writing). After downloading you’ll want to open up your project folder and create a new folder called scripts. Then extract the .zip into that folder.

After doing this you’ll start creating the actual map! You can use any text editor, but I recommend using Notepad++ or Atom. Firstly create a new file in your chosen editor and insert the below text, with the only variable to change being the background to the same colour you used earlier.

<!DOCTYPE html>
<html = style="height: 100%;">
		<title>DnD World Map</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<link rel="stylesheet" href="scripts/leaflet.css">
		<script src="scripts/leaflet.js"></script>
	<body style="height: 100%;margin: 0;">
		<div id="map" style="width: 100%; height: 100%; background: #000000;"></div>
		<script type="text/javascript">
	//Creating the Map
		var map ='map').setView([0, 0], 0);
		L.tileLayer('map/{z}/{x}/{y}.png', {
			continuousWorld: false,
			noWrap: true,	
			minZoom: 0,
			maxZoom: 10,
	//Coordinate Finder
		var marker = L.marker([0, 0], {
			draggable: true,
		marker.bindPopup('LatLng Marker').openPopup();
		marker.on('dragend', function(e) {

After adding this to the file, save it to your project folder and name it index.html. If you open the file now you should be greeted by your world map! There are a couple of variables that you will need to adjust depending on the size of your world map image.

For me, the Photoshop Script created folders from 0 to 6 – which correspond to different zoom levels. Your index.html file will open to your map at zoom level 0, which for me was far too zoomed out! To fix this I changed the minZoom variable to 3–which corresponds to pressing the + button 3 times. Adjust this until you are happy with the minimum zoom value.

You will also notice that if you press the ‘Zoom In’ button several times your map will disappear completely. This is because you will only be able to zoom in the number of folders available. To fix this, you will want to set your maxZoom to the highest folder number–for me this valve is 6. If you don’t like how close you can zoom in, decrease this number further.

After you’re happy with the minimum and maximum zoom levels you can then delete the folders outside of this range, this will save you space by not uploading unnecessary files.

Creating Map Markers

Now that you’ve added and configured your map in Leaflet.js it’s time to add some markers! You’ll notice that there is already one marker in the centre of your map named LatLng. This is what you’ll be using to create additional markers.

For this section I recommend creating a spreadsheet to list all of your different towns and places that you are wanting to put markers on. The spreadsheet should include:

  • A unique ID
  • X Coordinate (First number in bracket)
  • Y Coordinate (Second number in bracket)

Drag the marker around the map and make note of the X,Y coordinates and give each location a unique ID–I went from East to West across my map to make sure I collated all places. Don’t worry if you miss any, you can come back and add them later!

Now that you’ve created a list of all your markers/waypoints you can begin to add them to the map! You will need to create an individual line for each, and you will want to add them underneath //Markers in your HTML file. Here are several examples from my map from the above spreadsheet.

var el_gulndar = L.marker([36.0135, -106.3916]).bindPopup('<b>Gulndar</b>').addTo(map);
var el_teglhus = L.marker([44.4965, -100.7666]).bindPopup('<b>Teglhus</b>').addTo(map);
var el_ochri_college = L.marker([48.5166,-103.4692]).bindPopup('<b>Ochri College</b>').addTo(map);

As you can see in my example, it has created three markers on my world map which when clicked on show their place-name. The generic syntax for creating these markers is as follows, with the words in capitals needing to be changed.

var UNIQUE ID = L.marker([X VALUE, Y VALUE]).bindPopup('PLACE NAME').addTo(map)

After you’re happy that you have all of your markers in the right place and working, you can delete the text between //Coordinate Finder and //Markers as this is no longer required.

Grouping Markers

After adding your markers your world map might look messy due to the number of markers covering it, this is where grouping markers together is extremely useful!

The first stage to grouping your markers will be to remove some code from each marker. Currently, your markers include .addTo(map) which means that marker will be visible. Remove .addTo(map) from all of your markers, but make sure to leave the ; at the end of each line. This means that all markers will now disappear from your map.

How you decide to group your markers is entirely up to you! I broke mine up into the following categories:

  • Mage Colleges
  • Trading Posts
  • Cities
  • Towns
  • Forts/Castles
  • Temples

Once you have finalised your groups you’ll want to create your group variables–you will need one of these for each group. At the bottom of your marker section add the following code, again with the variables you need to change in capitals:

//Marker Groups
    var mg_GROUPNAME = L.layerGroup([LIST,OF,MARKERS]);
    var mg_ANOTHERGROUP = L.layerGroup([SOME,MORE,PLACES]);
//Marker Overlay
    var overlays={
    L.control.layers(null, overlays).addTo(map);

After changing all of the above variables the full marker code looked like this:

	var el_gulndar = L.marker([36.4919, -114.038]).bindPopup('<b>Gulndar</b>');
	var el_teglhus = L.marker([45.3058, -108.413]).bindPopup('<b>Teglhus</b>');
	var el_ochri_college = L.marker([48.5166,-111.2255]).bindPopup('<b>Ochri College</b>');
//Marker Groups
	var mg_towns = L.layerGroup([el_gulndar,el_teglhus]);
	var mg_towers = L.layerGroup([el_ochri_college]);
//Marker Overlay
	var overlays={
		"Towns" : mg_towns,
		"Towers" : mg_towers,
	L.control.layers(null, overlays).addTo(map);

When you now load your map, you will notice that all of your markers have disappeared! In order to see the markers, use the menu at the top right of the screen to hide and show marker groups!

Now depending on how you’re planning to use this map, you’ll either want to upload the contents of your project folder to the root directory of your website. This will mean the map will load instantly when you type in your url.

If you are wanting to do something similar to the below map, instead rename and upload the whole project folder then create an iframe on your website site using the below code structure:

<iframe src="https://URL/PROJECT FOLDER" width="100%" height="600"></iframe>

This project has been something I was really happy to complete. After nearly giving up I am extremely happy that I found Leaflet.js! I know that the above can be a little bit confusing so if you have any questions, drop a comment below and I’d be more than happy to answer them!

Project Result

This is now my updated map, which was built using Campaign Cartographer 3, on the version for my party the map has more than two zoom levels, but to save on bandwidth I have limited this demo version to only two. This version also features some additional features to the above tutorial such as custom map pins, I will update the tutorial to include this when I get a free moment. 🙂

Project Code

<!DOCTYPE = html>
<html style="height: 100%;">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="scripts/leaflet.css">
        <script src="scripts/leaflet.js"></script>

    <body style="height: 100%; margin: 0;">
        <div id="map" style="width: 100%; height: 100%; background: #53679f;"></div>
        <script type="text/javascript">
        //Creating the Map
            var map ='map').setView([0, 0], 0);
            L.tileLayer('tiles/{z}/{x}/{y}.png', {
                continuousWorld: false,
                noWrap: true,
                minZoom: 3,
                maxZoom: 6,
            var el_gulndar = L.marker([36.0135, -106.3916]).bindPopup('<b>Gulndar</b>');
            var el_teglhus = L.marker([44.4965, -100.7666]).bindPopup('<b>Teglhus</b>');
            var el_ochri_college = L.marker([48.5166,-103.4692]).bindPopup('<b>Ochri College</b>');
            var el_circle_of_the_land = L.marker([32.2871,-85.8691]).bindPopup('<b>Druid Camp</b>');
            var el_westhaven = L.marker([18.3128,-109.6875]).bindPopup('<b>Westhaven</b>');
            var el_glaenarm = L.marker([19.394,-92.5488]).bindPopup('<b>Glaenarm</b>');
            var el_mirstone = L.marker([14.4346,-64.9511]).bindPopup('<b>Mirstone</b>');
            var el_butterpond = L.marker([2.8991,-120.6738]).bindPopup('<b>Butterpond</b>');
            var el_pirin_post = L.marker([10.9196,-87.6269]).bindPopup('<b>Pirn Post</b>');
            var el_mistrith_keep = L.marker([-2.1088,-93.6035]).bindPopup('<b>Mistrith Keep</b>')
            var el_zimban = L.marker([-18.0623,-106.6113]).bindPopup('<b>Zimban</b>');
            var el_noblar = L.marker([-7.2752,-88.8574]).bindPopup('<b>Noblar</b>');
            var el_hommet_post = L.marker([-13.4751,-95.4052]).bindPopup("<b>Hommet's Trading Post</b>");
            var el_fangdor_fortress = L.marker([-12.8974,-125.332]).bindPopup('<b>Fangdor Fortress</b>');
            var el_tel_kibil = L.marker([-20.8793,-79.8925]).bindPopup('<b>Tel Kibil</b>');
            var el_saradim = L.marker([-37.7185,-107.4902]).bindPopup('<b>Saradim</b>');
            var el_ankoret = L.marker([-30.1451,-95.1855]).bindPopup('<b>Ankoret</b>');
            var el_bulasi_stables = L.marker([-41.9676,-94.2187]).bindPopup('<b>Bulasi Stables</b>');
            var el_cinders_college = L.marker([-50.5971,-91.4721]).bindPopup('<b>Cinders College</b>');
            var el_delarin_stronghold = L.marker([-36.7388,-83.9355]).bindPopup('<b>Delarin Stronghold</b>');
            var el_the_golden_palace = L.marker([-26.0567,-69.3237]).bindPopup('<b>The Golden Palace</b>');
            var el_the_soot_healer = L.marker([-0.2536,-70.9497]).bindPopup('<b>The Soot Healer</b>');
            var el_yarrow = L.marker([-26.1948,-52.8002]).bindPopup('<b>Yarrow</b>');
            var el_harron = L.marker([-42.8598,-44.165]).bindPopup('<b>Harron</b>');
            var el_tenbrie = L.marker([-55.8136,-46.0986]).bindPopup('<b>Tenbrie</b>');
            var el_umberlee = L.marker([-51.9442,-35.4638]).bindPopup('<b>Temple of Umberlee</b>');
            var el_dawsbury_post = L.marker([-17.4345,-40.6274]).bindPopup('<b>Dasbury Post</b>');
            var el_whitebridge_pass = L.marker([-3.8423,-40.5615]).bindPopup('<b>Whitebridge Pass</b>');
            var el_reedwater = L.marker([21.5144,-27.1801]).bindPopup('<b>Reedwater</b>');
            var el_nythi_asari = L.marker([-25.0258,-25.1147]).bindPopup('<b>Nythi Asari</b>');
            var el_layla_asari = L.marker([-12.1252,-12.788]).bindPopup('<b>Layla Asari</b>');
            var el_mystra = L.marker([-9.1671,-27.3339]).bindPopup('<b>Temple of Mystra</b>');
            var el_helm = L.marker([-0.2197,-53.4811]).bindPopup('<b>Temple of Helm</b>');
            var el_circle_of_the_moon = L.marker([-39.3852,-37.2656]).bindPopup('<b>Druid Camp</b>');
        //Maystrus Isle
            var ma_emyi_dorei = L.marker([9.2973,-0.1977]).bindPopup('<b>Emyi Dorei</b>');
            var ma_seiche_college = L.marker([26.3524,1.7138]).bindPopup('<b>Seiche College</b>');
        //Amspar Key
            var am_port_clulx = L.marker([-41.4262,-17.1606]).bindPopup('<b>Port Clulx</b>');
            var am_port_khel = L.marker([-28.8446,6.8994]).bindPopup('<b>Port Khel</b>');
            var or_jarrens_outpost = L.marker([14.4346,36.4746]).bindPopup("<b>Jarren's Outpost</b>");
            var or_myrefall = L.marker([13.5819,21.0058]).bindPopup('<b>Myrefall</b>');
            var or_hythe = L.marker([9.102,55.8105]).bindPopup('<b>Hythe</b>');
            var or_pavvs_stable = L.marker([-2.3065,42.2094]).bindPopup("<b>Pavv's Stable</b>");
            var or_guthram = L.marker([-4.5435,54.9975]).bindPopup('<b>Guthram</b>');
            var or_ballymena = L.marker([-12.0393,33.6621]).bindPopup('<b>Ballymena</b>');
            var or_eastcliff_crossroad = L.marker([-28.1495,35.2441]).bindPopup('<b>Eastcliff Crossroad</b>');
            var or_erast = L.marker([-34.3071,60.1611]).bindPopup('<b>Erast</b>');
            var or_grasshope = L.marker([-37.3177,20.2368]).bindPopup('<b>Grasshope</b>');
            var or_thornheart_college = L.marker([-44.9803,31.311]).bindPopup('<b>Thornheart College</b>');
            var or_torrine = L.marker([-22.1467,61.7431]).bindPopup('<b>Torrine</b>');
            var or_port_venzor = L.marker([-30.4486,87.2534]).bindPopup('<b>Port Venzor</b>');
            var or_greenflower = L.marker([-42.391,76.8603]).bindPopup('<b>Greenflower</b>');
            var or_bellmare = L.marker([-30.6946,125.0683]).bindPopup('<b>Bellmare: City of Fire</b>');
            var or_anyor = L.marker([-15.0296,111.3793]).bindPopup('<b>Anyor</b>');
            var or_aynor_post = L.marker([-10.5958,122.5854]).bindPopup('<b>Aynor Post</b>');
            var or_ormskirk = L.marker([-2.7455,111.1376]).bindPopup('<b>Ormskirk</b>');
            var or_garens_well = L.marker([9.1888,106.5893]).bindPopup("<b>Garen's Well</b>");
            var or_port_wormbourne = L.marker([20.3652,131.7919]).bindPopup("<b>Port Wormbourne</b>");
            var or_tamsworth = L.marker([20.5916,117.7075]).bindPopup("<b>Tamsworth</b>");
            var or_nuxvar = L.marker([20.5505,103.0078]).bindPopup("<b>Nuxvar</b>");
            var or_orion = L.marker([29.3438,87.9565]).bindPopup("<b>Orion: Capital of Oakla</b>");
            var or_skystead_nook = L.marker([19.9113,80.4638]).bindPopup("<b>Skystead Nook</b>");
            var or_bramblewoods = L.marker([4.4778,73.7622]).bindPopup("<b>Bramble Woods</b>");
            var or_lancer_gate = L.marker([7.6238,69.6972]).bindPopup("<b>Lancer Gate</b>");
            var or_leira = L.marker([44.887,54.9755]).bindPopup("<b>Temple of Leira</b>");
            var or_kelemvor = L.marker([-12.1252,79.475]).bindPopup("<b>Shrine of Kelemvor</b>");
            var or_mask = L.marker([-42.9242,71.2792]).bindPopup("<b>Shine of Mask</b>");
            var or_palace_plenty = L.marker([-15.2205,12.1289]).bindPopup("<b>Eiflin: The Palace Plenty</b>");
            var or_eldath = L.marker([-1.9112,11.3818]).bindPopup("<b>Temple of Eldath</b>");
            var or_bhel_thoram = L.marker([28.652,39.0673]).bindPopup("<b>Bhel Thoram</b>");
            var or_kelgrum = L.marker([39.09563,52.0532]).bindPopup("<b>Kelgrum</b>");
            var or_gar_dural = L.marker([23.7652,58.3154]).bindPopup("<b>Gar Dural</b>");
            var or_bhel_thoram2 = L.marker([27.3717,76.5087]).bindPopup("<b>Bhel Thoram</b>");
            var or_dblook_college = L.marker([31.0152,100.0195]).bindPopup("<b>Dblook College</b>");
            var or_gilnium_vineyards = L.marker([35.2994,111.5991]).bindPopup("<b>Gilnium Vineyards</b>");
            var or_pine_castle = L.marker([-4.5216,89.3408]).bindPopup("<b>Pine Castle</b>");
            var or_fort_eaglecrest = L.marker([-8.2114,112.6538]).bindPopup("<b>Fort Eaglecrest</b>");
            var or_knifeedge_creek = L.marker([-20.8177,109.0942]).bindPopup("<b>Knife-edge Creek</b>");
            var or_healthy_horse_post = L.marker([-11.4154,58.8427]).bindPopup("<b>The Healthy Horse Post</b>");
            var or_malar = L.marker([52.6563,96.8774]).bindPopup("<b>Temple of Malar</b>");
            var or_kejgrav = L.marker([54.7246,73.3007]).bindPopup("<b>Kejgrav</b>");
            var or_rukule_cross = L.marker([47.2344,88.1103]).bindPopup("<b>Rukule Cross</b>");
            var or_norreborg = L.marker([43.0046,108.9843]).bindPopup("<b>Nørborg</b>");
            var or_world_rod = L.marker([43.7234,77.7512]).bindPopup("<b>World Rod?</b>");
        //Marker Groups
            var mg_mage_colleges = L.layerGroup([el_ochri_college,el_cinders_college,ma_seiche_college,or_thornheart_college,or_dblook_college]);
            var mg_trading_posts = L.layerGroup([el_pirin_post,el_hommet_post,el_bulasi_stables,el_dawsbury_post,or_jarrens_outpost,or_pavvs_stable,or_eastcliff_crossroad,or_aynor_post,or_garens_well,or_skystead_nook,or_gilnium_vineyards,or_healthy_horse_post,or_rukule_cross]);
            var mg_cities = L.layerGroup([el_gulndar,el_glaenarm,el_butterpond,el_noblar,el_ankoret,el_yarrow,el_tenbrie,el_layla_asari,ma_emyi_dorei,or_myrefall,or_hythe,or_ballymena,or_grasshope,or_torrine,or_port_venzor,or_bellmare,or_ormskirk,or_port_wormbourne,or_orion,or_bhel_thoram,or_bhel_thoram2,or_kejgrav]);
            var mg_towns = L.layerGroup([el_westhaven,el_mirstone,el_zimban,el_tel_kibil,el_saradim,el_harron,el_reedwater,el_nythi_asari,am_port_clulx,am_port_khel,or_guthram,or_erast,or_greenflower,or_anyor,or_tamsworth,or_nuxvar,or_bramblewoods,or_kelgrum,or_gar_dural,or_norreborg]);
            var mg_forts = L.layerGroup([el_teglhus,el_mistrith_keep,el_fangdor_fortress,el_delarin_stronghold,el_the_golden_palace,el_whitebridge_pass,or_lancer_gate,or_palace_plenty,or_pine_castle,or_fort_eaglecrest,or_knifeedge_creek]);
            var mg_temples = L.layerGroup([el_circle_of_the_land,el_the_soot_healer,or_eldath,el_mystra,el_helm,or_leira,or_kelemvor,el_circle_of_the_moon,el_umberlee,or_mask,or_malar,or_world_rod]);
        //Marker Overlay
            var overlays = {
					"Mage Colleges" : mg_mage_colleges,
					"Trading Posts" : mg_trading_posts,
					"Cities" : mg_cities,
					"Towns" : mg_towns,
					"Forts/Castles" : mg_forts,
					"Temples" : mg_temples,
        //Marker Group Control
            L.control.layers(null, overlays).addTo(map);

by Sam Brooks

Sam is the founder and editor for Tech Trail. With a background in Broadcast Engineering, and great enthusiasm for smart home and emerging technologies.

More by Sam →

  • Twitter
  • KoFi
  • Email

Sam Brooks

Sam is the founder and editor for Tech Trail. A Broadcast Engineer with a passion for technology and design. Working on the bleeding edge of technology Sam is exposed to a vast amount of emerging technologies and likes to keep up to date on the latest tech in general.


    1. Warren

      I actually dont need this awsnered i did not see that u meanted the size of yours (: Is there a easy way to increase the number of zoom levels. looks like mine only outputed 6 levels. Maybe it does it automatically. based on the size of the map dimensions? If not is there a way to make it more or less? (Im not talking about the maxZoom)

      1. Hey Warren, Increasing the Map Image’s dimensions does indeed create more levels of zoom. An alternative that could potentially work would be changing the tile size in the Tile Cutter so each tiles is smaller or larger (but I haven’t tested this method).
        Do let me know if this answers your question 🙂

  1. Nico M

    Hey there Sam! This has been a blessing since I have made a very detailed map but it lacked that necessary interaction my players direlessly need for DnD, for that I have a question to your method of creating one; For the interactive markers you place, how elaborate can I add in terms of description, pictures, bios etc? I see in your pictures there are a ton of markers! But can I get really detailed in descriptions for each markers and not only the “names” per-se?

    1. Hey Nico,
      Glad to hear you’ve found the map tutorial useful! In terms of adding additional information to the popups, it does support some basic HTML tags such as
      to add new lines so can add some basic information about towns. In my testing, I sadly haven’t been able to add images to this popup.

      As an additional suggestion, you could make the city names into Hyperlinks, so when clicking links to a wiki-style page, I would recommend something like for this 🙂 this can be done by replacing the text inside the .bindPopup brackets with the following “<a href=’’><b>PlaceName</b></a>”

      I hope this helps 🙂

  2. Roger

    Hey, as someone who has 0,1% knowledge of html, I have to say this guide is proving to be extremely useful, so thanks!
    I had to look elsewhere for a tutorial on how to limit the borders of the map and even add some informative text to the markers, but it wasn’t nearly as comprehensive as what you did here. But there’s still one thing I’m missing… How did you use different colors in your markers? I’m guessing it’s something simple, but as I said I have almost 0 knowledge of hmtl :/

    1. Hey Roger, apologies for taking so long to reply! For the different coloured markers I designed them in Photoshop and added them to the /icons folder. To add them to your map you’ll need to add a little bit of extra code to your project. I am planning to update quite a few of my guides in the hopefully near future, but you can accomplish this following this page from the documentation here ( Please let me know if you have any issues!

  3. Njen

    Thank you for this fantastic tutorial. Everything is working perfectly except for one issue. The longitude coordinates seem to increase in distance to each other in some kind of mild exponential curve. The latitude coordinates look perfectly evenly spaced out though in a linear fashion. In the example linked below I have manually added in markers at regular steps of ’10’ in both axis, and as can clearly been seen, the y axis is not evenly spaced:
    Any idea on what could be causing this? I am using Leaflet 1.9.3

  4. Error

    Probably years too late, but worth a shot. Where did you get the custom map pin icons? The tutorial to set up the map worked fantastically, but I’d like to have different icons of map pins to help distinguish between locations. I’ve pored through the Internet for hours but can’t find any modern map pins with fantasy-style images within them. Would you happen to remember where you got them from?

Leave a Reply

Your email address will not be published.