A Beginner Friendly Guide to HTML Coding.

by steve-gibbs5 in Design > Software

88 Views, 3 Favorites, 0 Comments

A Beginner Friendly Guide to HTML Coding.

bus.gif
Thumbnail.png
Screenshot 2026-04-24 000717.png
20260419_211946~2.jpg
Tube.gif

Welcome dear reader. This is a companion Instructable to my Touchscreen Interactive Multi-Map Wall Console project where I show how I made the wall unit, graphics and animations, and the mention a bit about the coding. But to save making it too long, I decided to split it into two parts, this part as the title says, will guide through the actual HTML (Hypertext Markup Language) coding that made all the maps/games come to life, built using real-world web technology and live data. For this, I am using my Transport Ops map as the example as it's one of the less complicated web apps, but has a few different features.

On the surface, the map lets you:

  1. View a live map
  2. Toggle transport layers like buses and tube lines
  3. See traffic conditions
  4. Interact with on-screen markers that show real-time information

But as I mentioned, the real purpose of this project isn’t just the map itself… it’s understanding how the code behind it works, how each part connects, and how you can safely change things without everything falling apart. The inspiration to write this is to hopefully help others who are thinking how I was (and still am)... wanting to make something cool using code, but unsure or lacking confidence and knowledge to do so.

It's also worth mentioning, and this matters… I am still learning this coding game too. So this isn’t written from the perspective of someone who's been doing this for years (about 6 months at time of writing), it’s written from building something basic that worked, then breaking it repeatedly, figuring out why, making it work again, then improving on it. A beginners guide to HTML code, written from a beginners perspective. In Step 9, I have also included a bonus web app code specifically for this Instructable for you to play around with.

So lets get stuck in.

Supplies

Screenshot 2026-04-24 094716.png
Screenshot 2026-04-24 103731.png
Screenshot 2026-04-24 000717.png

I made and use my MCARS multi-map project with Widows 11 and using Chrome as my default browser, so I will be referring to this in the following steps, but I think this will also relate to other OS and browsers. You don’t really need anything complicated to get started:

  1. Notepad (perfectly fine for beginners. I mainly used this)
  2. Or Notepad++ (recommended, much easier to read code. I used this sometimes as well)
  3. A modern browser like Chrome or Edge
  4. An internet connection (needed for online maps and live data)
  5. An good online coding community for asking questions and advice.

Useful learning sites:

  1. Brilliant.org, Codecademy and W3Schools which is great for the basics
  2. Leaflet documentation (for maps)
  3. API keys free or paid (for live/up to date data)

What Is an HTML Web App

Screenshot 2026-04-24 095044.png
Screenshot 2026-04-24 095139.png

In its basic form, an HTML web application is a web page that contains three main parts working together:

  1. HTML: builds the structure (what you see on the screen)
  2. CSS: controls the appearance (how it looks, buttons, images, colours, text etc.)
  3. JavaScript: controls behaviour (makes the app function)

These three parts are broken down into the following...

  1. <html> → the container for the entire webpage, everything sits inside this
  2. <head> → setup area (links, settings, map libraries), not visible on screen
  3. <style> → where your CSS lives, this controls how everything looks
  4. <body> → everything you actually see on screen (map, buttons, layout)
  5. <script> → where your JavaScript lives, this controls what everything does

These function using a web browser and act as your own person web page . The map itself is powered by a library called Leaflet (my other projects also uses MapLibre), which handles things like zooming, panning, and placing markers.

The live data part comes from APIs, which are basically services on the internet that send data back when you ask for it, some are free to use, others you pay for.

A useful thing you can do to help yourself, especially as projects grow, is to leave clear notes inside your code, often called "Comments". These don’t affect how things run, they’re purely there for humans… including future you, who will absolutely forget what something did after a few weeks. In HTML, comments look like <!-- --> (for example, <!-- TRANSPORT MAP -->) and are useful for labelling sections or marking important areas of the layout. In CSS and JavaScript, you’ll usually see /* */ (/* MAP AREA */) for block comments, or in JavaScript also // (// TomTom Traffic Tile Layer) single line notes. The key is to use them to explain why something exists, not just what it is… a few well-placed, meaningful notes can come in very useful.

Below is the full code I use for my Transport Ops app which I will use as the example...

<!-- CREATOR - S GIBBS -->
<!-- MCARS - MAPPING CONTROL & AWARENESS RESPONSE-->
<!-- REACHABILITY MAP -->
<!-- 19/12/2025, STABLE VERSION V2 -->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LCARS TRANSPORT OPS</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>

<style>
html, body { margin:0; height:100%; background:#000; overflow:hidden; font-family:Impact, sans-serif; }

/* MAP AREA */
#map { position:absolute; top:50px; left:170px; right:40px; bottom:40px; z-index:1; }

/* LEFT RAIL FRAME */
#frame { position:absolute; inset:0; pointer-events:none; z-index:1000;
border-top:30px solid #777; border-left:20px solid #777; border-bottom:20px solid #777;
border-top-left-radius:100px; border-bottom-left-radius:100px;
}
#frame::after { content:""; position:absolute; inset:0; margin:20px;
border-top:8px solid #444; border-left:8px solid #444; border-bottom:8px solid #444;
border-top-left-radius:80px; border-bottom-left-radius:80px;
}


/* TITLE */
#title { position:absolute; top:0; left:110px; padding:6px 14px; font-size:25px; color:#73cb91; background:black; z-index:1001; }


/* CONTROLS */
#controls { position:absolute; top:115px; left:44px; width:130px; bottom:40px; z-index:999; display:flex; flex-direction:column; }
#controls button { margin:6px 8px; padding:10px; background:#222; color:#4fc3f7; border:3px solid #777; border-radius:14px; cursor:pointer; }
#controls button:hover { background:#4fc3f7; color:#000; }

/* PULSING ICONS */
.pulse-marker::after { content:''; position:absolute; width:100%; height:100%; left:0; top:0; border-radius:50%; box-shadow:0 0 8px 2px currentColor; animation:pulse 1.5s infinite; opacity:0.7; }
@keyframes pulse { 0%{transform:scale(1);opacity:0.7;}50%{transform:scale(1.5);opacity:0.3;}100%{transform:scale(1);opacity:0.7;} }

/* BLUE RECTANGLES ON RIGHT SIDE OF LEFT RAIL WITH BLACK END PADDING */
#blue-rect-top, #blue-rect-bottom {
position: absolute;
background: #4fc3f7;
z-index: 1001;
}

/* TOP RECTANGLE */
#blue-rect-top {
right: 220px;
top: 0px;
width: 500px;
height: 37px;
}
#blue-rect-top::before, #blue-rect-top::after {
content: "";
position: absolute;
width: 4px;
height: 100%;
background: black;
top: 0;
}
#blue-rect-top::before { left: -4px; }
#blue-rect-top::after { right: -4px; }

/* BOTTOM RECTANGLE */
#blue-rect-bottom {
right: 220px;
bottom: 0px;
width: 500px;
height: 20px;
}
#blue-rect-bottom::before, #blue-rect-bottom::after {
content: "";
position: absolute;
width: 4px;
height: 100%;
background: black;
top: 0;
}

#blue-rect-bottom::before { left: -4px; }
#blue-rect-bottom::after { right: -4px; }

/* PINK OVERLAY RAILS */
#pink-rail-1, #pink-rail-2 {
position: absolute;
left: 0px;
width: 25px;
z-index: 1002;
}

/* TOP PINK RAIL */
#pink-rail-1 {
top: 120px;
height: 150px;
background: #4fc3f7;
}

/* BOTTOM PINK RAIL */
#pink-rail-2 {
top: 370px;
height: 150px;
background:#73cb91;
}

/* BLACK END CAPS (TOP AND BOTTOM) */
#pink-rail-1::before,
#pink-rail-1::after,
#pink-rail-2::before,
#pink-rail-2::after {
content: "";
position: absolute;
left: 0;
width: 100%;
height: 4px;
background: black;
}

/* top caps */
#pink-rail-1::before,
#pink-rail-2::before {
top: 0;
}

/* bottom caps */
#pink-rail-1::after,
#pink-rail-2::after {
bottom: 0;
}#blue-rect-bottom::before, #blue-rect-bottom::after {
content: "";
position: absolute;
width: 4px;
height: 100%;
background: black;
top: 0;
}

#blue-rect-bottom::before { left: -4px; }
#blue-rect-bottom::after { right: -4px; }
</style>
</head>
<body>

<div id="frame"></div>
<div id="pink-rail-1"></div>
<div id="pink-rail-2"></div>
<div id="title">LCARS TRANSPORT OPS</div>

<div id="blue-rect-top"></div>
<div id="blue-rect-bottom"></div>

<div id="controls">
<button onclick="playClick(); setLayer()">ROAD MAP</button>
<button onclick="playClick(); toggleLayer('bus')">BUSES 🚌</button>
<button onclick="playClick(); refreshBuses()">REFRESH BUSES 🔄</button>
<button onclick="playClick(); toggleLayer('tube')">TUBE 🚇</button>
<button onclick="playClick(); toggleLayer('traffic')">TRAFFIC 🌍</button>
<button onclick="playClick(); toggleLayer('trainlines')">TRAIN LINES 🚉</button>
<button onclick="playClick(); toggleDarkMode()">LIGHT/DARK 🌗</button>
</div>

<div id="map"></div>

<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>

<script>
const TFL_KEY = "TFL API KEY GOES HERE";
const TOMTOM_KEY = "TOMTOM API KEY GOES HERE";
const THUNDER_API = "THUNDERFOREST API KEY GOES HERE";

function playClick(){
clickSound.currentTime = 0; // allows rapid re-clicks
clickSound.play();
}

const clickSound = new Audio("keyok3.mp3");

const map = L.map('map').setView([51.5074,-0.1278],12);

const lightMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png');
const darkMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png');

let darkMode = true;
darkMap.addTo(map);

const layers = { bus: L.layerGroup(), tube: L.layerGroup() };

// FIX: explicit DOM bindings (remove implicit globals)
const controls = document.getElementById("controls");

// Thunderforest Train Lines Tile Layer (toggleable)
const trainLayer = L.tileLayer(
`https://tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=${THUNDER_API}`,
{ opacity:0.8 }
);

// TomTom Traffic Tile Layer
const trafficLayer = L.tileLayer(
`https://{s}.api.tomtom.com/traffic/map/4/tile/flow/relative/{z}/{x}/{y}.png?key=${TOMTOM_KEY}`,
{ opacity:0.75 }
);

function toggleLayer(name){
if(name==='traffic'){
map.hasLayer(trafficLayer)
? map.removeLayer(trafficLayer)
: map.addLayer(trafficLayer);
return;
}

if(name==='trainlines'){
map.hasLayer(trainLayer)
? map.removeLayer(trainLayer)
: map.addLayer(trainLayer);
return;
}

const layer = layers[name];

if(map.hasLayer(layer)){
map.removeLayer(layer);
layer.clearLayers();
} else {
map.addLayer(layer);
if(name==='bus') loadBuses();
if(name==='tube') loadTube();
}
}

// Pulsing icons
const busIcon = L.divIcon({
html:`<div class="pulse-marker" style="font-size:22px;color:red;">🚌</div>`,
className:'',
iconSize:[24,24],
iconAnchor:[12,12]
});

const tubeIcon = L.divIcon({
html:`<div class="pulse-marker" style="font-size:22px;color:cyan;">🚇</div>`,
className:'',
iconSize:[24,24],
iconAnchor:[12,12]
});

function loadBuses(){
layers.bus.clearLayers();

const center = map.getCenter();
const offsets=[
{lat:0,lon:0},
{lat:0.03},
{lat:-0.03},
{lat:0.03,lon:0.03},
{lat:-0.03,lon:-0.03}
];

offsets.forEach(offset=>{
const lat=center.lat+(offset.lat||0);
const lon=center.lng+(offset.lon||0);

fetch(`https://api.tfl.gov.uk/StopPoint?lat=${lat}&lon=${lon}&stopTypes=NaptanPublicBusCoachTram&radius=2000&app_key=${TFL_KEY}`)
.then(r=>r.json())
.then(data=>{
if(!data.stopPoints) return;

data.stopPoints.slice(0,50).forEach(stop=>{
const marker=L.marker([stop.lat,stop.lon],{icon:busIcon}).addTo(layers.bus);

marker.on('click',()=>{
fetch(`https://api.tfl.gov.uk/StopPoint/${stop.id}/Arrivals?app_key=${TFL_KEY}`)
.then(r=>r.json())
.then(arr=>{
arr.sort((a,b)=>a.timeToStation-b.timeToStation);

let html=`<b>${stop.commonName}</b><hr>`;

arr.slice(0,5).forEach(a=>{
const dir=a.towards?`<small>Towards ${a.towards}</small><br>`:'';
html+=`<b>${a.lineName}</b> → ${a.destinationName}<br>${dir}⏱ ${Math.floor(a.timeToStation/60)} min<br><br>`;
});

marker.bindPopup(html).openPopup();
});
});
});
});
});
}

function refreshBuses(){ loadBuses(); }

function loadTube(){
layers.tube.clearLayers();

fetch(`https://api.tfl.gov.uk/Line/Mode/tube/Status?app_key=${TFL_KEY}`)
.then(r => r.json())
.then(data => {
data.forEach(line => {
const trainCount = 3 + Math.floor(Math.random()*3);

for(let i=0; i<trainCount; i++){
const lat = 51.5 + (Math.random() - 0.5) * 0.12;
const lon = -0.12 + (Math.random() - 0.5) * 0.12;

const nextStations = ['King’s Cross', 'Liverpool Street', 'Victoria', 'Waterloo', 'Oxford Circus', 'Bank', 'Green Park'];

const randomStations = [];
while(randomStations.length<3){
const s = nextStations[Math.floor(Math.random()*nextStations.length)];
if(!randomStations.includes(s)) randomStations.push(s);
}

const status = line.lineStatuses[0].statusSeverityDescription;

const html =
`<b>${line.name} Line</b><br>Status: ${status}<br>Next Stops:<br>` +
randomStations.map((s,k)=>`&#10148; ${s} in ${2+k*3} min`).join('<br>');

L.marker([lat, lon], {icon:tubeIcon})
.bindPopup(html)
.addTo(layers.tube);
}
});
});
}

function setLayer(){
map.eachLayer(l=>{
if(l!==lightMap && l!==darkMap && l!==trafficLayer && l!==trainLayer)
map.removeLayer(l);
});

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
}

function toggleDarkMode(){
darkMode=!darkMode;

if(darkMode){
map.removeLayer(lightMap);
darkMap.addTo(map);
} else{
map.removeLayer(darkMap);
lightMap.addTo(map);
}
}
</script>

<button onclick="window.close()" style="
position:fixed;
top:0px;
right:0px;
width:130px;
height:30px;
z-index:9999;
padding:3px;
background:#000000;
color:#00bbff;
border:none;
font-family:Impact;
font-size:18px;">

EXIT SYSTEM
</button>

</body>
</html>

Understanding the HTML Structure

Code 1.png

So, for anyone new to code, this will probably look a bit overwhelming at first glance… it certainly did for me. But once you start breaking it down, you realise it’s not one giant complicated thing, it’s just a handful of simple parts stacked together. The easiest way to think about it is like building a control panel… you start with the frame, then add the buttons, then wire up the logic behind it.

We’ll use the Transport Ops map as the example here.

1. The start of every HTML code (top section):

<!DOCTYPE html>
<html>
<head>

This is always the starting point. It’s basically telling the browser “this is a webpage, load it as such”. A simple start, but without it things will behave strangely.

You’ll also see things like:

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

This is setup so everything displays correctly, especially on different screen sizes. Without the viewport line for example, things can look zoomed out or broken on smaller screens.

2. Loading the map system (Leaflet):

<link rel="stylesheet" href="https://unpkg.com/leaflet...">
<script src="https://unpkg.com/leaflet..."></script>

This pulls in Leaflet, which if you remember, is the map engine doing all the heavy lifting. Without this, your <div id="map"> would just sit there as an empty box. Leaflet is what turns that box into an actual interactive map you can zoom, move, and add things to.

3. Styling everything (the <style> section):

This is where all the visual side of things lives… layout, colours, spacing, positioning, etc.

For example:

#map {
position:absolute;
top:50px; The larger the number, the further away from the top the map would be
left:170px; The same goes for these parts too (0 is far left of the screen)
right:40px;
bottom:40px;
}

This tells the map exactly where to sit on the screen, leaving space for your LCARS side panel and borders. In the above example, you don't really need all four positions, using Top and Left will be enough, but using all four can change an elements height and width, although there Height: and Width: codes for this.

You’ve also got things like:

  1. #controls: your button panel on the left
  2. #frame: the LCARS-style border wrapping the screen
  3. #blue-rect-top and bottom: those header/footer bars
  4. .pulse-marker: the glowing animation on buses and tube icons

So this entire section is purely about how things look and their locations, not what they do.

4. The actual visible layout (HTML body):

<div id="controls">...</div>
<div id="map"></div>

This is what actually appears on screen. Inside #controls, you have your buttons:

  1. ROAD MAP
  2. BUSES
  3. TUBE
  4. TRAFFIC
  5. etc.

Each button has something like:

onclick="playClick(); toggleLayer('bus')"

Which means "play a sound effect> then run a function". So the HTML is not just layout, it’s also connecting the buttons to the logic behind them.

5. Sounds and basic setup (JavaScript starts here):

const clickSound = new Audio("keyok3.mp3");

This loads your button sound file. keyok3.mp3 is the file path name of my sound effect file. I should note that the file format I used is .mp3, and although this does work for me, I read that it's advisable to use .wav files instead as they work better so I did this in all of my future projects that use sound. If you have MP3 files, you can convert them using a sound editor like Audacity. Simply open a new project, import the MP3, then export it saving it as a WAF file instead.

Then:

function playClick(){
clickSound.currentTime = 0;
clickSound.play();
}

This makes sure the sound plays every time you click, even if you click quickly if you watch the games in the MCARS video demo, you will see this in action with rapid fire. It’s a small detail, but it adds a lot to the feel of the interface.

6. Creating the map:

const map = L.map('map').setView([51.5074,-0.1278],12);

This is where the map actually comes to life.

  1. [51.5074, -0.1278] is London (latitude and longitude)
  2. 12 is the zoom level

Then you’ll see:

darkMap.addTo(map);

Which adds the visible map layer. Without this, you technically have a map… but nothing would be visible.

7. Layers (this is where cool things start to happen):

const layers = {
bus: L.layerGroup(),
tube: L.layerGroup()
};

Each layer is like its own container.

  1. One for buses
  2. One for tube

This lets you turn them on and off without affecting anything else. You also have separate tile layers like:

  1. Traffic (TomTom)
  2. Train lines (Thunderforest)

These behave slightly differently, but the idea is the same… they can be toggled independently.

8. Making the buttons do things:

function toggleLayer(name){

When you press something like BUSES:

  1. It checks if the layer is already on
  2. If it is, it removes it
  3. If not, it adds it

Then it calls functions like:

loadBuses();

Inside that, you’ll see markers being created:

L.marker([lat, lon], {icon:busIcon})

That’s what actually places the icons on the map.

9. Live data:

This is the clever part, but the idea is simple:

fetch(`https://api.tfl.gov.uk/...`)

This is asking TfL (Transport for London)... “Give me nearby bus stops and arrival times

Then:

.then(r => r.json())

This converts the response into something JavaScript can use. After that, the code loops through the results and creates markers with popups showing things like:

  1. Bus number
  2. Destination
  3. Time until arrival

So the process is:

  1. fetch = go get data
  2. then = do something with it

10. Simulated tube system (a bit of creative license):

Unlike buses, tube train tracking is not pulled live here. Instead:

  1. Random positions are generated
  2. Random “next stops” are chosen
  3. But actual line status is pulled from TfL

So it looks dynamic, gives real data, even though parts are simulated.

11. Dark mode toggle:

function toggleDarkMode(){

This simply switches between:

  1. Light map
  2. Dark map

It removes one layer and adds the other.

12. Exit button:

<button onclick="window.close()">

This is mainly for kiosk (running in full screen, no address or taskbars visible) or touchscreen setups.

NOTE… this only works properly in certain environments (like locally opened files or controlled systems), not always in normal browser tabs. How to use this is explained in Step7 of my MCARS Instructable.

In short…

If you strip everything back, the whole system is basically:

  1. Build the layout (HTML)
  2. Style it (CSS)
  3. Create the map (Leaflet)
  4. Add buttons
  5. Link buttons to functions
  6. Load data (optional but powerful)

Everything else is refinement. A very useful habit you should use is when you start experimenting, don’t work directly on your only copy, especially if it's a working one. Do this instead:

  1. Create a master or baseline file like, Transport Map.html: this is your clean, working version. Then make a second file, Transport Map TEST.html → this is your playground. Now you can...
  2. Change things
  3. Break things
  4. Experiment freely

And if it all goes wrong… just copy the baseline file text and paste it back in the test file and start again. It saves a lot of frustration, trust me on this.

Code Tags and Symbols

Screenshot 2026-04-24 100433.png

Tags:

Most things in HTML work in pairs, what you might call opening and closing tags.

For example:

<div id="map"></div>
  1. <div> → this is the opening tag, it starts something
  2. </div> → this is the closing tag, it ends it

Anything placed between those two sits inside that element. You’ll see this pattern everywhere:

<body> ... </body>
<style> ... </style>
<script> ... </script>

So the browser reads it like a set of containers, one inside another, a bit like boxes stacked inside boxes. A couple of useful things to know:

  1. The closing tag always has a / in it
  2. If you forget to close something, the page can behave strangely
  3. Some tags don’t need closing (like <meta>), but most do

In simple terms, opening tags start something, closing tags finish it… and everything in between is what they control. Once you get used to that idea, the whole structure of a webpage becomes much easier to follow.

Symbols:

  1. { } these define a block of code, like a container. You’ll see them in CSS and JavaScript
  2. Example:
#map { ... }
  1. Everything inside the braces belongs to that rule or function
  2. : this means set this to this”, used mainly in CSS and objects
  3. Example:
color: red;
  1. Which reads as “colour equals red
  2. ; this marks the end of a line or instruction. Think of it like a full stop at the end of a sentence. Without it, things can run together and stop working properly

So in plain terms: { } = the container : = assigns a value ; = ends the instruction. Small symbols… but if one goes missing, the whole thing can and will stop working.

HTML Recap (Hyper Text Markup Language)

Screenshot 2026-04-24 100719.png

So lets recap the three separate code formats individually. The HTML is the foundation and it tells the browser what elements exist.

For example:

<div id="map"></div>

This line creates an empty box. It doesn’t look like much, but this is where the entire map gets drawn. Another important section:

<div id="controls">

This creates the container for your buttons. Inside it, you’ll see things like:

<button onclick="toggleLayer('bus')">BUSES 🚌</button>

This does two things at once:

  1. Displays a button labelled “BUSES”
  2. Tells the browser to run a function called toggleLayer when it’s clicked

So HTML isn’t just layout… it also links to behaviour.

CSS Recap (Cascading Style Sheets)

Screenshot 2026-04-24 100943.png

On to CSS, this is where beginners often experiment first, because it’s visual and more hands-on, think of it like drawing with code.

Example:

#controls button {
background:#222;
color:#4fc3f7;
}

Let's look at this in more detail and use a button as an example.

  1. background controls the background colour of the button
  2. color controls the text colour inside the button

These are not the same thing. You could have a dark background with bright text, or the opposite.

Colours: What is #222 or #4fc3f7?

These are hex colour codes. A hex code always starts with # and is followed by characters, like:

#RRGGBB

RR = red, GG = green, BB = blue. Each pair ranges from 00 (none) to FF (maximum). So:

  1. #000000 = black
  2. #ffffff = white
  3. #ff0000 = red
  4. #222 is actually shorthand for #222222, which is a dark grey.

You can also use colour names instead. For example:

background: black;
color: cyan;

But not all colours have names, and named colours are less precise. Hex codes give you full control.

Project Layouts:

#map {
position:absolute;
top:50px;
left:170px;
right:40px;
bottom:40px;
}

This positions items on the screen, like our map example. Instead of saying “width 500px”, this says:

  1. Stay 50px from the top
  2. 170px from the left
  3. 40px from the right
  4. 40px from the bottom

So the map stretches to fit the space.

Animation (Pulsing Marker):

@keyframes pulse {

This defines an animation. Then:

animation:pulse 1.5s infinite;

This means:

  1. Run the animation
  2. Take 1.5 seconds
  3. Repeat forever

How I made my codes were to give them basic controls and styling, make the app fully functional, then finish off by playing around with button shapes, colours, text etc. To keep in with my Star Trek LCARS system theme, I made the buttons pill shaped. The code would look like this:

.button {
padding:12px 40px;
border-radius:40px;
background:#9ea4ba;
color:black;
cursor:pointer;
}

The border-radius:40px; is what gives the buttons the rounded sides. So with the user interface set up, we need to make it functional.

Javascript Recap (Also Known As JS)

Screenshot 2026-04-24 101047.png

This is where everything actually happens, the functionality. For our transport app, we need to make it usable.

Creating the Map:

const map = L.map('map').setView([51.5074,-0.1278],12);

This line:

  1. Finds the HTML element with id "map"
  2. Creates a Leaflet map inside it
  3. Centers it on London
  4. Sets zoom level to 12

You can change the coordinates to move the starting location.

Layers (Different Map Styles):

const lightMap = L.tileLayer(...)
const darkMap = L.tileLayer(...)

These are different map backgrounds to create a light mode and a dark mode, so only one of these maps are shown at a time.

Buttons Trigger Functions:

<button onclick="toggleLayer('bus')">

This connects our HTML code to JavaScript. When clicked, it runs:

function toggleLayer(name){

Inside that function, the app decides if it should show this layer or hide it.

Fetching Data (Talking to APIs):

fetch(`https://api.tfl.gov.uk/...`)

This sends a request to TfL. Think of it like “Give me nearby bus stops and arrival times”. The API sends back data, which our code then processes.

Adding Markers:

L.marker([lat, lon], {icon:busIcon})

This places an icon on the map. When clicked, it can show a popup with information. An icon can be an emoji or an image file, even an animated GIF file.

So with the code setup explained, there are some things we need to know to make it work correctly.

Spelling and Text Format

CODE 2.png

I live in the U.K so I use U.K spelling, and this is something I found out pretty early on in my coding journey and something that caused me a little frustration at first... spelling matters, but not everywhere.

In CSS (important):

CSS uses American spelling, so:

color: red;

works perfectly, but:

colour: red;

does nothing at all… it’s simply ignored. As I was using the letter 'U' from the start, I couldn't figure out why a simple experiment code didn't work. It wasn't until I did a little online research that I figured out why. The same goes for things like... background-color (works), background-colour (does not). So in CSS, you must use American spelling only.

In HTML:

HTML doesn’t really use words like that for styling, so spelling isn’t an issue in the same way. But that said, for attribute names, spelling still matters:

<div id="map"></div> (lower case works)
<div Id="map"></div> (UPPERCASE does not)

So it’s about matching the exact expected names.

In JavaScript:

JavaScript is very strict. If you create something like:

let colour = "red";

Then later write:

console.log(color);

That will fail… because color and colour are treated as completely different variables. So in JS, spelling must match exactly, every time. I found out that since most coding languages and documentation use American spelling, it’s usually easier to go with... color, center, behavior, etc.

Adding File Paths:

This is not used in our transport code example, but something to note. If you use a full file path (i.e, right click a sound effect file, click "Copy as path"), when you paste it into your code you will get something like "C:\Users\your name\My Project\my picture.png".

"C:\Users\my name\My Project\my picture.png" (A copied path has backslashes which wont work)
"C:/Users/my name/My Project/my picture.png" (This is the correct format)
Example... <img id="player" src="C:/Users/my name/Pictures/my project/my picture.png">

First thing, you only need one pair of quote marks, is if copy and pasting, check this. Second, the pasted path has back slashes, these need to be changed to forward slashes or you file wont execute, so make sure you change each one.

In simple terms:

  1. CSS: must use American spelling
  2. HTML: exact names matter, not UK/US spelling
  3. JavaScript: spelling must match exactly

A single missing letter won’t just look wrong… it can quietly break things without telling you why as I found out. Using something like Notepad++ can help you spot mistakes, but I found out that it won’t reliably detect them, especially with coding-specific spelling like color vs colour, because to the editor it’s all just text. What it does do well is syntax highlighting, so correct keywords are coloured and incorrect ones often aren’t, which can give you a visual clue something’s off. It won’t warn you about broken logic or wrong variable names either, so if something stops working, it’s usually down to you to track it down. What is very helpful for actual error checking, is doing it in your browser… open your file, press F12, and check the console, as that’s where JavaScript errors will usually show up.

What You Can Change Safely, and What Needs a Bit More Care

colours.png

This is the part where you start experimenting… and honestly, this is where I learnt the most. But it helps to know the difference between things you can change safely, and things that need a bit more attention. First, the safe stuff… this is where you can play around without worrying about breaking the whole app.

Things like:

  1. Colours in the CSS
  2. Text labels (button names, titles, etc.)
  3. Button sizes and spacing
  4. The starting position of the map (those latitude/longitude numbers)
  5. The emojis used for icons

All of these only affect how the app looks, not how it actually works. So if something looks wrong, you can just change it back. Nothing critical will stop working.

For example, changing a colour like:

background:#222;

to something else won’t break anything… worst case, it just looks a bit ugly until you fix it.

Now, the next group… this is where you slow down slightly.

Things like:

  1. Functions such as toggleLayer()
  2. The fetch() URLs that pull in data
  3. Layer logic (adding and removing map layers)

You can change these… but this is where things are connected behind the scenes (covered in the next step). For example:

  1. If you rename a function in one place but not another, the button will stop working
  2. If you change part of a fetch URL incorrectly, the data just won’t load
  3. If you remove part of the layer logic, markers might not appear, or might never disappear

This is usually where beginners hit that moment of “it worked five minutes ago, now it doesn’t”. And the reason is almost always something small:

  1. A variable name doesn’t match anymore
  2. A function isn’t being called
  3. A URL has been slightly altered

So the approach here is simple, change one thing at a time… test it… then move on. If you treat this part carefully, you won’t break the system… and more importantly, you’ll actually understand how it works as you go.

Connected Code and Variables

Code 3.png

I touched on this in the last step, and this is one of the most important ideas to get your head around… because this is where code stops being lines on a page, and starts behaving like a connected system.

Take this example:

const layers = { bus: L.layerGroup() };

Here you’re creating something called layers, and inside it you’ve got a bus layer stored under the name bus. Later on, you’ll see:

layers.bus.addTo(map);

Now this line is reaching back to that earlier definition. It’s saying "Go to layers, find the thing called bus, and add it to the map", so even though these lines are in completely different parts of the code, they’re linked together by that shared name. It's the same idea with functions. You define one here:

function loadBuses(){

That’s where the actual work happens… fetching data, placing markers, etc. Then somewhere else, often triggered by a button, you’ll see:

if(name==='bus') loadBuses();

This is what actually 'calls' that function and makes it run. So:

  1. One place defines it
  2. Another place uses it

Now here’s where you can get caught out. If you change the name in one place, like this:

function loadBusData(){

…but forget to update this part:

loadBuses();

The connection is broken. The code will still run, but that function will never be found or executed, so nothing happens. This is why it helps to think of code less like separate lines, and more like a system of references or links.

  1. Names must match exactly
  2. One part often depends on another
  3. Changing something in isolation can quietly break the connection

It’s a bit like wiring... everything might look fine on the surface, but if one connection is off, that part of the system just stops responding. Once you understand that, debugging becomes a lot easier… because you know to follow the chain, not just stare at one line.

Variables:

An important piece to add here is variables, because they’re what make all these connections possible.

A variable is simply a named container for a piece of data. You give something a name, store a value in it, and then reuse that name elsewhere instead of rewriting the value every time.

For example:

const TFL_KEY = "YOUR API KEY WOULD BE HERE";

Here, TFL_KEY is the variable. Instead of typing your API key everywhere in the code, you store it once and then reuse it wherever it’s needed. So later on, when you see something like:

fetch(`https://api.tfl.gov.uk/...&app_key=${TFL_KEY}`)

It’s pulling that value from the variable. The same idea applies to things like:

const map = L.map('map');

Now map becomes a reference you can use throughout the code, instead of recreating it every time. The important thing to understand is:

  1. A variable’s name must be used exactly the same way everywhere
  2. If you change the name in one place, you must change it everywhere else it’s used
  3. Variables are what allow different parts of the code to talk to each other

So when you see code working across multiple sections, it’s often variables quietly linking everything together behind the scenes.

API Keys

Reachability API.png

To gather the data, this map app uses three API keys, Transport for London (TFL), TomTom, and ThunderForest (for the trainlines map). TomTom and Thunder are global maps, where as TFL is for London data only (not much use if you live in other parts of the world), but this can easily be replaced with a transport API service for your location. You’ll need that provider’s API key, endpoints, and data format, then update the fetch() links and how the data is read (lat/lon, arrivals, etc.) to match—same logic, just different data source behind it.

const TFL_KEY = "YOUR API KEY WOULD GO HERE";

This is your API key, which is basically a permission token. It tells the TfL system who you are and allows your app to request data. Without it, the request either won’t work at all, or it will be heavily limited.

Changing the Key:

If you get your own key, you simply replace the value inside the quote marks:

const TFL_KEY = "NEW API KEY GOES HERE";

That’s all you need to do at this level. You don’t touch anything else around it.

Where That Key Is Actually Used:

Further down in the code, you’ll see it being inserted into a request, like this:

...&app_key=${TFL_KEY}

This is called a template string, and it’s just a way of dropping your key into the URL automatically. So instead of hardcoding the key everywhere, you define it once at the top, and the code reuses it wherever needed. This keeps things cleaner and avoids mistakes. Think of it as a variable.

Important: Changing Location (Why the Key Alone Isn’t Enough):

This is where a lot of beginners may get caught out. You might think “If I change the API key, I can just use this for another city”, but it doesn’t work like that. Take this line:

https://api.tfl.gov.uk/StopPoint?lat=...&lon=...

This URL is specifically designed for Transport for London. It knows about London bus stops, London routes, London data. So even if you replace the key, the request is still asking “Give me London transport data”.

Example – Switching to New York:

If you wanted to build the same idea for New York for example, you would need to change more than just the key. You would need:

  1. A completely different API (for example, something from New York’s MTA or another data provider)
  2. A different request URL (because each API has its own format)
  3. Possibly different data handling, because the structure of the returned data may not match TfL

So instead of just swapping this:

const TFL_KEY = "NEW_KEY";

You’d also be rewriting things like:

fetch("https://api.tfl.gov.uk/...")

and adjusting how the results are read and displayed.

To gather the data, this map app uses three API keys, Transport for London (TFL), TomTom, and Thunder (for the trainlines map). TomTom and Thunder are global maps, where as TFL is for London data only (not much use if you live in other parts of the world), but this can easily be replaced with a transport API service for your location. You’ll need that provider’s API key, endpoints, and data format, then update the fetch() links and how the data is read (lat/lon, arrivals, etc.) to match—same logic, just different data source behind it.

Example: Replace TFL with New York (MTA-style API)

The original (TFL) fetch from my code:

fetch(`https://api.tfl.gov.uk/StopPoint?lat=${lat}&lon=${lon}&app_key=${TFL_KEY}`)

Replace it with an MTA (Metropolitan Transportation Authority) endpoint:

fetch(`https://api-endpoint.mta.info/vehicles?lat=${lat}&lon=${lon}&api_key=YOUR_KEY`)

Then adjust how you read the data from TFL:

stop.lat
stop.lon
stop.commonName

to MTA-style:

vehicle.latitude
vehicle.longitude
vehicle.label

Marker example:

L.marker([vehicle.latitude, vehicle.longitude])
.bindPopup(vehicle.label)
.addTo(layers.bus);

In short, swap the URL, add the new API key, update the data fields (lat/lon/names). You will have the same system, but with a different data feed. And treat your API keys like you would your bank card pin code, never share it. If you think someone else is using it, go to the supplier, delete the key and generate a new one, then add this to your code, replacing the old one.

One thing I want to mention to save you any potential headaches, there are free and paid for API keys available from different services. The ones I use are free to use and have usage limitations, perfectly fine for personal use as long as I don't make hundreds of calls a day. That said, I did make an air traffic app using a free OpenSky key which I was pleased with, tracking planes and helicopters and clicking on them to see the aircraft data. I had the basic functions working and was happy with it through light testing. I came back to it the next day... no planes were showing at all. I checked the code, asked for help on forums, but no joy. I put to one side and worked on something else. I came back to it and it was working again (I tried it for less than 2 minutes). I focused on the styling, tried it again, the planes were gone again. It turned out it was the API key was harshly limiting the data, very temperamental and kinda sucked. So at time of writing, I was using OpenSky's own public air traffic tracker wrapped in a HTML background and adding some of my own data until I find a better solution.

A Web App Code to Experiment With

Screenshot 2026-04-24 103240.png

The following code is something I made specifically for this Instructable. It is a basic World Clock Map web app that uses MapLibre for that actual maps, and a couple of usable buttons to toggle between light and dark mode, and to clear the drop pins, and no API keys to worry about. Tap/click anywhere on the map to drop a little pin and a popup will display with the current time/date for that location. I wrote this code so you can copy/paste it, study it along with this Instructable, then have a play around with the code, changing button styles, colours, and maybe use it as a baseline to add other fun and useful features.

<!-- APP CREATOR - S GIBBS -->
<!-- THIS IS A SIMPLE WORLD CLOCK MAP FOR HTML CODING BEGINNERS -->
<!-- CLICK ANYWHERE ON THE MAP TO SHOW THAT AREAS CURRENT TIME AND DATE -->
<!-- USE THE CLEAR MARKER BUTTON TO CLEAR ALL OF THE DROP PINS -->
<!-- USE THE LIGHT/DARK BUTTON TO CHANGE BETWEEN LIGHT AND DARK MAPS WHICH ALSO CHANGES THE POPUP COLOURS -->

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Global HUD Map</title>

<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet"/>
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>

<style>

body {
margin:0;
background:#000;
font-family:Arial, sans-serif;
}

/* FRAME */
#frame {
position:absolute;
inset:10px;
border:2px solid #00bbff;
pointer-events:none;
}

/* MAP */
#map {
position:absolute;
top:20px;
left:20px;
right:20px;
bottom:20px;
}

/* CONTROLS */
#controls {
position:absolute;
top:30px;
left:30px;
z-index:10;
display:flex;
flex-direction:column;
}

button {
margin:5px 0;
padding:10px;
background:#111;
color:#00bbff;
border:1px solid #00bbff;
cursor:pointer;
}

button:hover {
background:#00bbff;
color:#000;
}

/* =========================
POPUP HUD STYLING
========================= */

/* DARK MODE POPUP */
.popup-dark .maplibregl-popup-content {
background:#111;
color:#00ffcc;
border:1px solid #00ffcc;
font-size:13px;
letter-spacing:0.5px;
}

/* LIGHT MODE POPUP (HUD DARK STYLE for readability) */
.popup-light .maplibregl-popup-content {
background:#1a1a1a;
color:#00bbff;
border:1px solid #00bbff;
font-size:13px;
letter-spacing:0.5px;
}

/* TIME DISPLAY BLOCK */
.time-display {
font-family: monospace;
white-space: nowrap;
}

</style>
</head>

<body>

<div id="frame"></div>

<div id="controls">
<button onclick="toggleTheme()">LIGHT / DARK</button>
<button onclick="clearMarkers()">CLEAR MARKERS</button>
</div>

<div id="map"></div>

<script>

// ========================
// MAP INITIALISATION
// ========================

let darkMode = true;

const map = new maplibregl.Map({
container: 'map',
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
center: [0, 20],
zoom: 2
});

let markers = [];

// ========================
// CLICK TO DROP MARKER
// LIVE GLOBAL TIME SYSTEM
// ========================

map.on('click', function(e){

const lng = e.lngLat.lng;

/*
============================
TIMEZONE ESTIMATION METHOD
============================

Earth is divided into ~24 time zones.
Each zone = roughly 15 degrees longitude.

So:
longitude / 15 ≈ UTC offset
*/

const offset = Math.round(lng / 15);

const popupClass = darkMode ? "popup-dark" : "popup-light";

/*
Create stable HTML container for popup
(prevents rendering glitches)
*/
const container = document.createElement("div");

const label = document.createElement("div");
label.textContent = "LOCAL TIME";

const timeBlock = document.createElement("div");
timeBlock.className = "time-display";

container.appendChild(label);
container.appendChild(timeBlock);

/*
LIVE CLOCK FUNCTION
updates every second
*/
function updateClock(){

const now = new Date();

const localTime = new Date(
now.getTime() + offset * 60 * 60 * 1000
);

/*
IMPORTANT:
Using textContent inside a dedicated element
avoids rendering artifacts in the final digit
*/
timeBlock.textContent = localTime.toLocaleString();
}

updateClock();

const timer = setInterval(updateClock, 1000);

/*
CREATE POPUP
*/
const popup = new maplibregl.Popup({
offset: 25,
className: popupClass
}).setDOMContent(container);

/*
CREATE MARKER
*/
const marker = new maplibregl.Marker({ color: "#00ffcc" })
.setLngLat([lng, e.lngLat.lat])
.setPopup(popup)
.addTo(map);

marker.togglePopup();

/*
STORE MARKER + TIMER
*/
markers.push({ marker, timer });

});

// ========================
// CLEAR ALL MARKERS
// ========================

function clearMarkers(){

markers.forEach(obj => {
obj.marker.remove();
clearInterval(obj.timer);
});

markers = [];
}

// ========================
// THEME SWITCH
// ========================

function toggleTheme(){

darkMode = !darkMode;

if(darkMode){
map.setStyle('https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json');
} else {
map.setStyle('https://basemaps.cartocdn.com/gl/positron-gl-style/style.json');
}
}

/*
==============================
BEGINNER NOTES
==============================

WHAT THIS MAP DOES:

- Click anywhere on the world map
- A marker is placed instantly
- A popup opens automatically
- The popup shows LIVE local time
- Time updates every second

HOW THE TIME WORKS:

We estimate timezone using:
longitude ÷ 15

This gives a rough UTC offset.
It is not perfectly accurate, but works well for learning.

IMPORTANT CONCEPTS:

1. FUNCTIONS
updateClock() runs repeatedly to keep time live

2. INTERVALS
setInterval runs code every 1000ms (1 second)

3. DOM ELEMENTS
Popup content is built using HTML elements
instead of plain text for stability

4. MARKER SYSTEM
Each marker stores its own timer
so everything can be cleaned properly later

THINGS YOU CAN EXPERIMENT WITH:

- Change marker colour
- Change frame colour
- Adjust zoom level
- Change popup labels
- Switch map style (dark/light)

KEY IDEA:

Click → create marker → start timer → display live system output

This is the foundation of interactive mapping systems.
*/

</script>

</body>
</html>

To use this, open Notepad on your PC, open a new tab, copy the code above then paste it into the empty Notepad page. Then you need to save it a particular way because Notepad will save anything as a .txt file by default. Click File > Save > name your app file something like "World Clock" but write it as follows... World Clock.html as it's the .html that turns it from a simple text file to an actual working web app.

To get to the code to make changes, there are two options:

  1. Go to your map file location (your Pictures or Documents folder) > right click the file > select Open with > then select Notepad.
  2. Or open Notepad > File > Open > in the bottom corner where it says Text documents, click the drop down and select All files > then click on the map file.

One thing to note, a simple static image of a world map will display visually in a web app, but is no good for an app like this because it has no underlying structure for the code to interact with, it’s just pixels on a screen. The application needs real geographic data, coordinates, zoom levels (seeing the whole globe right down to seeing roads), and event handling (like clicks and positions), which a picture simply cannot provide. Without a mapping engine, the code has nothing underneath to calculate locations from or place dynamic markers onto. That’s why tools like MapLibre are essential, they turn the map into a live, interactive system rather than just a visual background.

Conclusion

20260419_212009~2.jpg
20260419_212049~2.jpg
20260419_212647~2.jpg

And that brings us to a close to this coding lesson. If you take anything away from this, it should be that... you don’t need to understand everything at once. What matters is:

  1. Knowing what each part roughly does
  2. Knowing what you can safely change
  3. And not being afraid to break things

Because breaking things is how this project was built in the first place. And if you keep going, step by step, changing things, testing things, You stop copying code and start building your own systems which, I imagine, is exactly where you intended to end up which it is for me.

I do hope that this Instructable gives you the confidence and inspiration to give this a go yourself, it was, and still is a challenge for me, but it's one I'm pleased I took on because I love using my MCARS Multi-map console and quietly proud of what I have achieved so far.

Thanks for reading, and happy making.