HTML is not a programming language; it's a serialization format for a tree. The browser parses it into the DOM (Document Object Model) โ a live tree of element objects that CSS styles and JavaScript manipulates. Learn to see the tree and HTML becomes trivial.
<div class="card meal-form" id="mealForm"> โ element with attributes
<textarea id="descInput" maxlength="500"
placeholder="What did you eat?"></textarea>
<button type="button" class="btn primary" id="saveBtn">Add meal</button>
</div> โ closing tag ends the subtree
That's a simplified slice of the real meal form in
templates/log.html. The grammar: a tag names the element
type, attributes are key="value" metadata, children nest inside until
the closing tag. Two attributes matter constantly:
id โ a unique handle. Ndiro's JavaScript finds
elements almost exclusively by it:
document.getElementById('saveBtn').class โ non-unique labels for styling. An element
can carry several (class="card meal-form" has two), and
CSS selects on them.Tags carry semantics. <button>,
<nav>, <label>,
<h1> tell the browser (and screen readers, and
keyboard navigation) what a thing is; CSS separately decides
what it looks like. <div> and
<span> are the semantic-free containers (block and
inline respectively) you use when nothing more specific fits โ layout
scaffolding like .card.
Ndiro's fiber guide dropdown is a nice case study: each result row is
a real <button>, not a styled div with a
click handler:
// A real <button> gives keyboard/AT users click + Enter/Space for free.
const row = document.createElement('button');
row.type = 'button';
row.className = 'guide-row';
A div with a click listener works for mouse users only.
The native element brings focusability, keyboard activation, and
assistive-technology announcements at zero cost. Rule of thumb: if it
acts like a button, make it a <button>; if it
navigates, make it an <a href>.
The meal form uses half the input catalog โ each
type changes keyboard, validation, and picker UI,
especially on phones:
| Element | In the meal form | Notes |
|---|---|---|
<textarea> | description, context | Multi-line text; maxlength is client-side UX โ the server re-checks the 500 limit |
<input type="number"> | grams | min/step constrain the spinner, not the API |
<input type="date"> / "time" | meal date/time | Native pickers; values are YYYY-MM-DD / HH:MM strings |
<input type="file" accept="image/*"> | photo | Hidden; a styled button forwards clicks to it โ a standard trick, see photoBtn |
<input type="search"> | fiber guide lookup | Like text, plus a clear affordance |
<label for="fiberInput"> | the grams/date/time fields | Clicking the label focuses the input; screen readers announce it. (The two textareas rely on placeholder alone โ a real accessibility trade-off you can spot in the markup.) |
One subtlety worth stealing: every button in the form says
type="button". Inside a form, a button's default type is
submit, which triggers the browser's own form submission
(a full-page POST + reload). Ndiro handles saving in JavaScript, so each
button opts out. Forgetting this is a classic "why does the page reload
when I click?" bug.
templates/base.html holds the one skeleton every page
shares:
<!DOCTYPE html> โ "parse me as modern HTML"
<html lang="en">
<head> โ metadata, not visible content:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ndiro</title> โ the browser tab
<style>โฆ</style> โ all CSS (the CSS chapter)
</head>
<body> โ everything you see
โฆ
<script>โฆ</script> โ JS at the end, so the DOM above exists when it runs
</body>
</html>
The viewport meta is why the site isn't a zoomed-out
desktop page on a phone. And script placement is ordering, not style:
scripts execute where the parser meets them, so code at the bottom of
<body> can safely call
getElementById on anything above it.
HTML is to the page what a Hive table schema is to a query: pure structure declaration. The browser's parser is famously forgiving โ unclosed tags get auto-repaired rather than rejected, like a lenient SerDe. That forgiveness is why validation can't live in the markup and why the server re-checks everything.
On the log page, press F12 โ Elements.
Expand the tree to #mealForm. Click elements and watch
them highlight on the page. Now double-click the "Add meal" button's
text in the panel and rename it โ you're editing the live DOM
(reload to undo). Compare with Ctrl+U: view-source shows
the HTML as served; Elements shows the tree as it is now,
including everything JavaScript added.