Skip to content
EzzyWeb

Level 1, Foundations, Chapter 3 of 24

More HTML — Forms, Tables & Semantic Tags

Why this matters

So far you've structured content using generic building blocks like headings and paragraphs, but real pages need more than that: they need to describe entire regions of a layout, collect information from a visitor with many kinds of fields, and sometimes present data in rows and columns that span multiple cells. Every login screen, every checkout form, every pricing comparison table you've ever used on the web is built from the tags in this chapter.

These tools also matter more than they might first appear, because they carry meaning that browsers, search engines, and assistive technology can all understand automatically. A page built with the right semantic tags, properly labeled and validated form fields, and a well-structured table is not just easier for you to read as code, it's also more usable for people relying on screen readers and more discoverable for search engines, all without writing a single line of JavaScript.

The lesson

Why semantic tags matter

In the last chapter, every region of your page would likely have been wrapped in generic containers if you needed a container at all. HTML actually provides a set of semantic elements that describe the purpose of a section, not just its position. Tags like header, nav, main, section, article, and footer look and behave like plain containers by default, but they carry meaning: header marks introductory content for a page or section, nav marks a block of navigation links, main marks the primary content of the page (and should appear only once per page), and footer marks closing content like copyright notices or contact details. Using these instead of an unlabeled container makes your structure self-documenting, and it directly helps screen reader users, who can jump straight to the navigation or the main content instead of listening through an entire page.

A page laid out with semantic tags
HTML
1<header>2  <h1>Community Garden Club</h1>3  <nav>4    <a href="#events">Events</a>5    <a href="#contact">Contact</a>6  </nav>7</header>89<main>10  <section id="events">11    <h2>Upcoming Events</h2>12    <p>Join us for our monthly planting day this weekend.</p>13  </section>14</main>1516<footer>17  <p>&copy; 2024 Community Garden Club</p>18</footer>
Each region has a tag that names its purpose. A screen reader or a search engine can now tell where navigation, main content, and footer information begin and end without guessing.

Two more semantic tags are worth knowing well because they're easy to mix up: article and section. Use article for a piece of content that would make sense on its own, standing outside the page, like a blog post, a news story, or a single product card, something you could drop into a completely different page and it would still be complete. Use section for a thematic grouping of content that belongs together within the current page but isn't meant to stand alone, like a chapter of a longer document or a themed group of items on a homepage. A helpful test: if you'd be comfortable syndicating just that chunk of content elsewhere with its meaning intact, it's an article; if it only makes sense as part of the surrounding page, it's a section. aside marks content that's related but tangential, like a sidebar of related links or a pull quote, content a visitor could skip without losing the main point.

Two more tags round out the semantic toolkit. figure groups an image (or other media) together with its caption, and figcaption provides that caption; using them together instead of a plain img and p tells assistive technology that the two are linked. time marks a specific date or time in a way machines can parse reliably, using a datetime attribute in a standard format even if the visible text is written more casually, like 'last Tuesday'.

article, aside, figure, and time together
HTML
1<article>2  <h2>Our First Harvest</h2>3  <p>Published on <time datetime="2024-06-03">June 3rd</time>.</p>4  <figure>5    <img src="harvest.jpg" alt="Basket of freshly picked tomatoes" />6    <figcaption>This year's first tomato harvest</figcaption>7  </figure>8  <p>The tomatoes came in early this year thanks to the mild spring.</p>9</article>1011<aside>12  <h3>Related</h3>13  <p>Read our guide to composting.</p>14</aside>
The article stands on its own as a complete piece of content. The time tag gives a machine-readable date. The figure ties the image to its caption, and the aside holds tangential content.

Collecting input with forms

A form is how a page collects information from a visitor, such as a search query, a login, or a comment. The whole form lives inside a form tag, and individual fields inside it are usually input elements. An input's type attribute controls what kind of field it is: type="text" for a single line of text, type="email" for an email address (which also enables basic format checking in the browser), type="password" for hidden text, and type="checkbox" for a toggle, among others. Every input that collects meaningful data should be paired with a label element, connected using the label's for attribute matching the input's id attribute. This pairing isn't decorative: it means clicking the label text focuses the input, and it means a screen reader announces the label whenever a visitor lands on that field, so skipping it makes a form genuinely harder to use for many people.

A simple labeled form
HTML
1<form>2  <label for="name">Your name</label>3  <input type="text" id="name" name="name" />45  <label for="email">Your email</label>6  <input type="email" id="email" name="email" />78  <button type="submit">Sign Up</button>9</form>
Each input's id matches its label's for attribute, creating a real connection between the two rather than just visual proximity. The button's type="submit" marks it as the action that sends the form.

Beyond text, email, and password, several other input types are worth knowing well. type="number" restricts entry to numeric values and often adds small up and down arrows. type="date" shows a native date picker. type="radio" presents a set of mutually exclusive choices; radio inputs only behave as a group when they share the same name attribute, which is what tells the browser they belong to the same question. type="checkbox" is for independent on-or-off choices, and any number of checkboxes in a group can be checked at once. type="file" lets a visitor choose a file from their device. type="range" shows a slider between a minimum and maximum. type="color" opens a color picker.

A tour of input types
HTML
1<label for="qty">Quantity</label>2<input type="number" id="qty" name="qty" />34<label for="visit">Visit date</label>5<input type="date" id="visit" name="visit" />67<p>Size:</p>8<label><input type="radio" name="size" value="s" /> Small</label>9<label><input type="radio" name="size" value="m" /> Medium</label>1011<label><input type="checkbox" name="gift" /> Gift wrap this order</label>1213<label for="photo">Upload a photo</label>14<input type="file" id="photo" name="photo" />1516<label for="volume">Volume</label>17<input type="range" id="volume" name="volume" min="0" max="10" />1819<label for="theme">Pick a color</label>20<input type="color" id="theme" name="theme" />
The two radio inputs share name="size", which is what makes them mutually exclusive as a group; without a shared name, both could be selected at once like checkboxes.

Not every field is a single-line input. A textarea is for longer, multi-line text like a comment or message, and unlike input it wraps around its default text rather than using a value attribute. A select presents a dropdown list of choices, built from option tags inside it; adding the selected attribute to one option makes it the default. When a group of fields belongs together conceptually, like a shipping address, you can wrap them in a fieldset, with a legend as its heading, which screen readers announce when a visitor enters that group of fields.

textarea, select, and fieldset
HTML
1<fieldset>2  <legend>Delivery details</legend>34  <label for="notes">Delivery notes</label>5  <textarea id="notes" name="notes" rows="3"></textarea>67  <label for="country">Country</label>8  <select id="country" name="country">9    <option value="us" selected>United States</option>10    <option value="ca">Canada</option>11    <option value="mx">Mexico</option>12  </select>13</fieldset>
The fieldset and legend group two related fields under a shared heading. The select's first option carries selected, so United States shows by default when the page loads.

The browser can also check entries before a form is ever submitted, using validation attributes. required prevents submission until a field has a value. placeholder shows faint example text inside an empty field, useful as a hint but never a substitute for a real label since it disappears the moment someone types. min and max bound numeric or date values, while minlength and maxlength bound the number of characters in a text field. For more advanced cases, pattern accepts a regular expression the value must match, such as requiring a specific format for a product code.

A form using validation attributes
HTML
1<form>2  <label for="username">Username</label>3  <input4    type="text"5    id="username"6    name="username"7    placeholder="e.g. river_hiker"8    required9    minlength="3"10    maxlength="20"11  />1213  <label for="age">Age</label>14  <input type="number" id="age" name="age" min="13" max="120" required />1516  <button type="submit">Create account</button>17</form>
The browser refuses to submit this form until username has between 3 and 20 characters and age is a number between 13 and 120, all without a line of JavaScript.

Finally, a form's buttons matter more than they might seem. type="submit" sends the form's data; it's the default type for a button placed inside a form, which is a common source of accidental submissions. type="button" does nothing on its own and is meant for behavior you'll wire up yourself later with JavaScript. type="reset" clears every field in the form back to its original values, which is rarely what visitors actually want and is worth using sparingly. You can build a submit control either as <button type="submit">Save</button> or <input type="submit" value="Save" />; the button element is generally more flexible because it can contain other HTML like an icon, while the input version can only show plain text.

Notice that none of these forms actually send data anywhere yet, and that's expected at this stage. Later, once you learn JavaScript, you'll be able to react when a form is submitted, validate entries beyond what the browser does automatically, and send that data elsewhere. For now, the goal is simply to structure a form so its fields are clear, labeled, validated, and ready to be wired up.

Tables for genuinely tabular data

A table is the right tag when you have data that truly belongs in rows and columns, like a class schedule, a price comparison, or sports statistics; it is not a layout tool for arranging unrelated content side by side, which is a mistake common in older websites and worth avoiding. A table starts with the table tag, and inside it, each row is a tr (table row). Within a row, header cells use th and regular data cells use td. It's common to wrap your header row inside a thead section and your data rows inside a tbody section, which makes the structure clearer even though it isn't strictly required for a table to work.

A basic data table
HTML
1<table>2  <thead>3    <tr>4      <th>Day</th>5      <th>Class</th>6    </tr>7  </thead>8  <tbody>9    <tr>10      <td>Monday</td>11      <td>Pottery</td>12    </tr>13    <tr>14      <td>Wednesday</td>15      <td>Guitar</td>16    </tr>17  </tbody>18</table>
The thead row uses th cells to label each column, while the tbody rows use td cells for the actual values. Screen readers can announce column headers as a visitor moves through each cell.

A few extra table features handle real-world data much better. A caption tag, placed right after the opening table tag, gives the whole table a title that screen readers announce before reading any cells. On a th, the scope attribute (scope="col" or scope="row") tells assistive technology explicitly whether that header describes a column or a row, which matters once a table has headers on both edges. colspan stretches a cell across multiple columns, and rowspan stretches it down multiple rows, both useful when a label applies to more than one cell at once.

A table with a caption, scope, and spans
HTML
1<table>2  <caption>Weekend Class Schedule</caption>3  <thead>4    <tr>5      <th scope="col">Time</th>6      <th scope="col">Saturday</th>7      <th scope="col">Sunday</th>8    </tr>9  </thead>10  <tbody>11    <tr>12      <th scope="row">9 AM</th>13      <td colspan="2">Studio closed for cleaning</td>14    </tr>15    <tr>16      <th scope="row">10 AM</th>17      <td rowspan="2">Pottery (both days)</td>18      <td>Guitar</td>19    </tr>20    <tr>21      <th scope="row">11 AM</th>22      <td>Guitar</td>23    </tr>24  </tbody>25</table>
caption titles the whole table. scope tells screen readers which headers apply to columns versus rows. colspan merges the 9 AM row across both days, and rowspan lets Pottery span two time slots.

Bringing this chapter together with what came before, you now have a genuinely complete toolkit for structuring real content: headings and paragraphs for text, lists for grouped items, links and images for connecting and illustrating, semantic tags for describing regions and standalone pieces of a page, forms with a wide range of validated field types for collecting input, and tables with captions, scoped headers, and spanning cells for tabular data. Every page you'll build from here forward, no matter how styled or interactive it becomes in later chapters, starts from this same set of structural decisions, so it's worth returning to this chapter whenever you're unsure which tag best describes a piece of content.

Try it yourself

Try adding a required checkbox with its own label to the form, wrap the fields in a fieldset with a legend, and add a caption plus one spanning cell to the table.

Live preview

Chapter boss project

Build a contact and pricing page

Build a single page for a small business that includes proper semantic structure: a header with a title and navigation, a main content area, and a footer. Inside main, include an article describing the business (something that would still make sense if reposted elsewhere) and an aside with a tangential link, like a related blog post.

Add a contact form with at least five labeled fields covering a mix of types (for example a text name field, an email field, a radio group for preferred contact method sharing one name, a select for a topic, and a textarea for the message), grouped inside a fieldset with a legend, plus a submit button. Add required, placeholder, and at least one min/max or minlength/maxlength attribute somewhere in the form.

Add a pricing table with a caption, at least three rows of services or products and their prices, thead and tbody, th cells using the correct scope, and at least one cell using colspan or rowspan.

Before submitting your work, double check every label is correctly paired with its input by matching for and id exactly, confirm your table only uses th for header cells and td for data cells, and confirm your submit button is type="submit" while any non-submitting button is type="button".

  • Sketch the page regions first: header, main, footer, and decide what belongs in each before writing tags.
  • Radio inputs need the same name attribute to behave as a single mutually exclusive group; each still needs its own id and a matching label for.
  • Match each label's for attribute to the exact id of its input, including capitalization.
  • Build the table structure (table, caption, thead, tbody, tr, th, td) before filling in the real data so the nesting stays clear.
  • Reach for colspan when one cell's meaning spans multiple columns, and rowspan when it spans multiple rows.

Level-up checklist

Tick these off once each one is true for you.