๐ŸฅฆNdiro
Chapter 4 of 13

HTML: the document

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.

Elements, attributes, nesting

<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:

Meaning, not appearance

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:

templates/log.html โ†—
// 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>.

Forms and inputs

The meal form uses half the input catalog โ€” each type changes keyboard, validation, and picker UI, especially on phones:

ElementIn the meal formNotes
<textarea>description, contextMulti-line text; maxlength is client-side UX โ€” the server re-checks the 500 limit
<input type="number">gramsmin/step constrain the spinner, not the API
<input type="date"> / "time"meal date/timeNative pickers; values are YYYY-MM-DD / HH:MM strings
<input type="file" accept="image/*">photoHidden; a styled button forwards clicks to it โ€” a standard trick, see photoBtn
<input type="search">fiber guide lookupLike text, plus a clear affordance
<label for="fiberInput">the grams/date/time fieldsClicking 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.

The document skeleton

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.

๐Ÿ˜ From your world

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.

๐Ÿ”ฌ Try it

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.

๐Ÿ