One Screen at a Time

Chapter 1

Put one fact on the screen

Why I built this#

I am a seasoned developer. I came to the web when Notepad was the editor of choice for most enthusiasts and Flash was a big deal. (Does anyone remember ActionScript?) Then came advanced CSS, jQuery (still around, surprisingly!), PHP and a lot more. It was all quite complex, and it needed setup, servers and the whole mythical shebang. That is a long-winded way of saying I am 50. :)

I always wanted control over my work and a deeper understanding of it. Web components looked like the perfect combination: no framework and full control, yet close to what the world had picked as its tool of choice, the JavaScript frameworks.

When I was introduced to them, I could only think of rewriting the basics: my own button, my own dropdown. Then I hit an invisible barrier. I could not think of any other element worth building. This was the pre-AI era, and almost nothing explained web components the way I wanted to use them. Most tutorials went straight to Shadow DOM and <template>. I wanted plain DOM, with CSS from the page still able to reach every element, and full control.

Then a colleague joined me at work (hi, Yahor!), and I had to teach him whatever I was working on. It turned out that components were a perfect way to teach JavaScript at a higher level. They also helped me get past some of the obstacles my framework had at the time. And that was it. Like magic, I was stepping up my game and reaching a level of build quality in my apps that I thought I would never achieve.

This book is the tutorial I wished I had found: one small, real app, built one component at a time.


The task#

By the end of this chapter you will have a page that opens, loads a fact from a file, and shows it big and bold on a coloured screen. Reload and you get a different one.

It is a small thing, and that is the point. Every part of the finished app hangs off what you build in the next twenty minutes: the data, the page, and the loading step that joins them.

facts.json  ──fetch──▶  app.js  ──textContent──▶  the page

You need a text editor, a browser, and Python (or Node) to run a tiny local server. Nothing else. No npm install.


Step 1: Start with the data#

Most tutorials start with the page. Start with the data instead. Once you know what one fact looks like, the page almost designs itself.

Create a folder called microlearn and, inside it, a file called facts.json:

{
  "facts": [
    {
      "id": "b0",
      "t": "space",
      "f": "A day on Venus is longer than its year.",
      "m": "Venus turns once every 243 Earth days but circles the Sun in about 225. It also spins backwards, so the Sun rises in the west."
    },
    {
      "id": "b1",
      "t": "space",
      "f": "A teaspoon of neutron star would weigh billions of tonnes.",
      "m": "A neutron star squeezes more than the Sun's mass into a ball about 20 kilometres across. Nothing on Earth comes close to that density."
    }
  ]
}

Add four or five more of your own. Each fact has four parts:

Key Meaning Example
id A unique label. We will need it later to remember what you have seen. "b0"
t The topic. Later it decides the card's colour. "space"
f The fact: one short, surprising sentence. The headline. "A day on Venus…"
m More: the explanation that makes it stick. "Venus turns once…"

The short key names are a choice. When the app holds hundreds of facts, short keys keep the file small. The cost is that you have to remember what f and m mean. You will, because there are only four.

Four JSON rules that cause most of the grief#

JSON looks like JavaScript, but it is stricter:

  1. Double quotes only. 'single' quotes are an error.
  2. No trailing commas. A comma after the last item breaks the whole file.
  3. No comments. Not even //.
  4. Keys are always quoted. { f: "x" } is JavaScript. { "f": "x" } is JSON.

When something fails to load later in this chapter, one of these four is the likely cause. Paste your file into any JSON validator if you are unsure.


Step 2: The page#

Create index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Microlearn</title>
  <link rel="icon" href="data:,">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main id="stage" aria-live="polite">
    <article class="card">
      <h1 class="fact">Loading…</h1>
      <p class="more"></p>
    </article>
  </main>
  <script type="module" src="app.js"></script>
</body>
</html>

Four details are worth a second look:

  • <link rel="icon" href="data:,"> is an empty icon. Browsers always ask for /favicon.ico, and without this line your console fills with a harmless but distracting 404. Replace it with a real icon when you ship.
  • <h1 class="fact">Loading…</h1> is real content, so the page is never blank. If the script fails, the reader sees "Loading…" instead of a white void. Every failure in this book ends in a visible message, never a blank screen.
  • aria-live="polite" tells screen readers to announce changes inside <main>. It costs nothing now and saves a retrofit later.
  • type="module" is the one flag that matters. A module script can use import, runs after the page has been read, and lets you use await at the top level. You will lean on all three.

Now style.css. Do not worry about every line, only about the idea: the card fills the screen and centres its content.

:root {
  --bg: hsl(250 60% 91%);
  --ink: hsl(250 48% 13%);
  --accent: hsl(250 70% 32%);
  --f-display: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  --f-body: Georgia, "Times New Roman", serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--f-body);
}

.card {
  min-height: 100vh;
  max-width: 760px;
  margin-inline: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.1rem;
}

.fact {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 6vw, 3.35rem);
  line-height: 1.05;
  letter-spacing: -0.022em;
  text-wrap: balance;
}

.more {
  margin: 0;
  max-width: 34em;
  font-size: 1.15rem;
  line-height: 1.58;
}

Notice the colours are written as variables at the top (--bg, --ink). That looks like ceremony for a page with one colour. The Final touches section pays it back: dark mode becomes a matter of changing four lines.

The headline size uses clamp(min, preferred, max). It grows with the screen width (6vw), but never below 1.6rem or above 3.35rem. One line of CSS replaces a stack of media queries.


Step 3: Why you cannot just double-click the file#

Open index.html by double-clicking it. You will see "Loading…" and nothing else. In the next step we use fetch to read facts.json, and browsers refuse to fetch local files for security reasons. The address bar will say file:///…, and that is the catch.

The fix is to serve the folder over HTTP. From inside your microlearn folder:

python3 -m http.server 8765

Then open http://localhost:8765. (No Python? npx serve does the same.)

Leave that terminal running while you work. It is not a build tool. It only hands files to the browser, exactly as a real web server would, and the finished app will need nothing more.


Step 4: Fetch the facts#

Create app.js. We will write it in small pieces, then put it together.

Piece one: grab the elements we will change.

const stage = document.querySelector('#stage');
const factEl = stage.querySelector('.fact');
const moreEl = stage.querySelector('.more');

querySelector takes a CSS selector and returns the first match. We look up .fact and .more inside #stage once, at the top, rather than every time we need them.

Piece two: load the file.

async function loadFacts() {
  const res = await fetch('facts.json');
  if (!res.ok) throw new Error(`Could not load facts (${res.status})`);
  const data = await res.json();
  return data.facts;
}

Read this slowly, because it is the pattern behind every network call you will ever write:

  1. fetch('facts.json') asks the server for the file. It returns a promise, a value that arrives later. await pauses the function until it does.
  2. A 404 is not an error to fetch. If the file is missing, fetch still succeeds and hands you a response with ok: false. It only throws when the network itself fails. So we check res.ok ourselves and throw.
  3. res.json() reads the body and parses it into real JavaScript objects. If your JSON has a trailing comma, this is the line that fails, with a message like Unexpected token.
  4. We return data.facts, the array. Callers get a list, not the wrapper object.

Piece three: pick one fact.

function pickRandom(list) {
  return list[Math.floor(Math.random() * list.length)];
}

Math.random() gives a number from 0 up to, but not including, 1. Multiply by the list length and you get 0 up to the length. Math.floor rounds down, giving a valid index. With six facts you get 0 to 5, never 6.

Piece four: put it on the page.

function show(fact) {
  factEl.textContent = fact.f;
  moreEl.textContent = fact.m;
}

We use textContent, not innerHTML. The two look the same until a fact contains < or &. textContent treats whatever you give it as plain text. innerHTML treats it as markup, which means a fact containing <img onerror=…> becomes code that runs. Our facts are ours, so it would not matter today. The habit matters the first time data comes from somewhere you do not control. Make textContent your default and reach for innerHTML only for markup you wrote yourself.

Piece five: run it, and handle failure.

try {
  show(pickRandom(await loadFacts()));
} catch (err) {
  factEl.textContent = 'Something went wrong.';
  moreEl.textContent = err.message;
}

The top-level await works because the script is a module (remember type="module"). Anything that throws inside loadFacts lands in catch, and the reader sees a message instead of a frozen "Loading…".

Put together, the whole file:

const stage = document.querySelector('#stage');
const factEl = stage.querySelector('.fact');
const moreEl = stage.querySelector('.more');

async function loadFacts() {
  const res = await fetch('facts.json');
  if (!res.ok) throw new Error(`Could not load facts (${res.status})`);
  const data = await res.json();
  return data.facts;
}

function pickRandom(list) {
  return list[Math.floor(Math.random() * list.length)];
}

function show(fact) {
  factEl.textContent = fact.f;
  moreEl.textContent = fact.m;
}

try {
  show(pickRandom(await loadFacts()));
} catch (err) {
  factEl.textContent = 'Something went wrong.';
  moreEl.textContent = err.message;
}

Thirty lines. Four small functions, each with one job: load, pick, show, and a wrapper that keeps the page honest if any of them fails. That separation is the habit this whole book builds on.


Check it works#

Open http://localhost:8765 and reload a few times. You should see:

  • A large fact and a smaller explanation on a lavender background.
  • A different fact on most reloads (with six facts, you will sometimes get the same one twice in a row. That is chance, not a bug).

If something is wrong:

You see Probably
"Loading…" forever You opened the file directly. Use the server from Step 3.
"Something went wrong. Could not load facts (404)" facts.json is not in the same folder as index.html, or is misspelled.
"Something went wrong. Unexpected token…" A JSON rule from Step 1: look for a trailing comma or single quotes.
A blank page, no text at all Open the browser's developer tools (F12) and read the Console tab.

Try breaking it on purpose. Rename facts.json to fact.json, reload, and read the message. Then add a trailing comma to the file and reload again. Seeing these messages once, calmly, makes them far less frightening at midnight.


What just happened#

You separated three things that beginners often tangle together: the data (facts.json), the structure (index.html) and the behaviour (app.js). The data is plain JSON, so anyone can add a fact without touching code. The page works before any script runs. And the script is four tiny functions that each do one thing and fail loudly, so a mistake points at itself.

Nothing here is clever. That is deliberate. In the next chapter, a single line of this code (pickRandom) is replaced by a loop, and one fact becomes a feed.


Your turn#

  1. Easy. Add three of your own facts to facts.json, reload until you see each one. Give one a topic that does not exist yet, such as "food". Does anything break? (Not yet. It will matter in Chapter 2.)
  2. Stretch. Make a Show another button that picks a new fact without reloading the page. You will need addEventListener('click', …) and the pickRandom and show functions you already have. Do not rewrite them.

Checkpoint#

The finished code for this chapter is in code/ch01. Open it to see it running, or compare it with your own version.

Next#

Chapter 2 asks a question whose answer looks obvious and is not: how do you make a page that scrolls through facts one screen at a time, and knows which one you are looking at?