Complete HTML Course Module 8: Intermediate: Media and Embedded Content

Intermediate Project: Build a Complete Contact Page

  • Beginner
  • Lesson 8.4
  • 7 min read
  • 40 of 69 in the course

In short

In this HTML project you will build a complete contact page step by step, with a semantic layout, a validated contact form, an embedded map and an opening hours table. What you will build You are going to build the contact page for "Green Leaf Cafe", a small cafe.

In this HTML project you will build a complete contact page step by step, with a semantic layout, a validated contact form, an embedded map and an opening hours table.

What you will build

You are going to build the contact page for "Green Leaf Cafe", a small cafe. It uses almost everything from this part of the course, in one real page. When you finish, you will have:

  1. A complete head section with title, description and viewport.
  2. A semantic layout with header, nav, main and footer.
  3. A contact form with labels, the right input types and HTML validation.
  4. A table of opening hours with a caption and header cells.
  5. An embedded map with a link to a larger version.

The page won't look pretty yet, because there is no CSS. That is fine. A well-structured HTML page is the foundation that CSS builds on. Create a new folder called contact-page and a file inside it called contact.html.

Step 1: The head and the semantic page layout

Start with the skeleton. Write a good title and meta description, then add the landmarks: a <header> with the cafe name and navigation, a <main> for the content, and a <footer>.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contact Us - Green Leaf Cafe</title>
  <meta name="description" content="Contact Green Leaf Cafe in London. Send us a message, see our opening hours and find us on the map.">
  <link rel="stylesheet" href="css/styles.css">
  <link rel="icon" href="images/favicon.svg" type="image/svg+xml">
</head>
<body>
  <header>
    <p><a href="/">Green Leaf Cafe</a></p>
    <nav aria-label="Main">
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/menu">Menu</a></li>
        <li><a href="/contact" aria-current="page">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Contact us</h1>
    <p>Questions, bookings or feedback? We would love to hear from you.</p>
  </main>

  <footer>
    <p>© 2026 Green Leaf Cafe</p>
  </footer>
</body>
</html>

Open the file in your browser. You should see the cafe name, a list of three links, the "Contact us" heading and the footer. The aria-current="page" attribute tells screen reader users which page they are on in the menu.

Step 2: Add the contact form with validation

Inside <main>, after the intro paragraph, add a <section> for the form. Use the right input types, connect every label, add name to every field, and add validation rules. Group the "reason for contact" options in a <fieldset>.

<section aria-labelledby="form-heading">
  <h2 id="form-heading">Send us a message</h2>
  <p>Fields marked * are required.</p>

  <form action="/contact" method="post">
    <p>
      <label for="name">Your name *</label><br>
      <input type="text" id="name" name="name" required minlength="2" maxlength="80" autocomplete="name">
    </p>

    <p>
      <label for="email">Email address *</label><br>
      <input type="email" id="email" name="email" required autocomplete="email">
    </p>

    <p>
      <label for="phone">Phone number (optional)</label><br>
      <input type="tel" id="phone" name="phone" autocomplete="tel">
    </p>

    <fieldset>
      <legend>What is your message about? *</legend>
      <p>
        <input type="radio" id="reason-booking" name="reason" value="booking" required>
        <label for="reason-booking">Table booking</label>
      </p>
      <p>
        <input type="radio" id="reason-event" name="reason" value="event">
        <label for="reason-event">Private event</label>
      </p>
      <p>
        <input type="radio" id="reason-other" name="reason" value="other">
        <label for="reason-other">Something else</label>
      </p>
    </fieldset>

    <p>
      <label for="message">Message *</label><br>
      <textarea id="message" name="message" rows="6" required minlength="10" maxlength="1000"></textarea>
    </p>

    <p>
      <input type="checkbox" id="privacy" name="privacy" value="agreed" required>
      <label for="privacy">I agree that Green Leaf Cafe can use my details to reply to me.</label>
    </p>

    <p>
      <button type="submit">Send message</button>
    </p>
  </form>
</section>

Try to send the empty form. The browser stops you at the name field. Fill it in and try again: it now stops at the email. With radio buttons, required on just one button in the group is enough to make the whole group required.

Note: This form needs a server program at /contact to receive and handle the data. Until you have one, clicking "Send message" on a valid form shows an error page. That's expected. Remember: the server must check all the data again.

Step 3: Add the opening hours table

Opening hours are perfect tabular data: days down the side, times across the top. Add this new section after the form section, still inside <main>. Use colspan for the closed day.

<section aria-labelledby="hours-heading">
  <h2 id="hours-heading">Opening hours</h2>
  <table>
    <caption>Green Leaf Cafe opening hours</caption>
    <thead>
      <tr>
        <th scope="col">Day</th>
        <th scope="col">Opens</th>
        <th scope="col">Closes</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Monday</th>
        <td colspan="2">Closed</td>
      </tr>
      <tr>
        <th scope="row">Tuesday – Friday</th>
        <td><time>08:00</time></td>
        <td><time>18:00</time></td>
      </tr>
      <tr>
        <th scope="row">Saturday</th>
        <td><time>09:00</time></td>
        <td><time>20:00</time></td>
      </tr>
      <tr>
        <th scope="row">Sunday</th>
        <td><time>10:00</time></td>
        <td><time>16:00</time></td>
      </tr>
    </tbody>
  </table>
</section>

You will see a table with three columns. "Closed" stretches across both time columns on Monday. The <time> element marks each time in a way computers can read.

Step 4: Add the map and the address

Add a final section with the address and an embedded map. Use the <address> element for contact details: it is the semantic element for the contact information of a person or organisation. Give the iframe a clear title and lazy loading.

<section aria-labelledby="find-heading">
  <h2 id="find-heading">Find us</h2>
  <address>
    Green Leaf Cafe<br>
    12 Example Street<br>
    London WC2N 5DN<br>
    Phone: <a href="tel:+442079460000">020 7946 0000</a><br>
    Email: <a href="mailto:hello@greenleafcafe.example">hello@greenleafcafe.example</a>
  </address>

  <iframe
    width="600"
    height="400"
    src="https://www.openstreetmap.org/export/embed.html?bbox=-0.1298%2C51.5055%2C-0.1218%2C51.5095&amp;layer=mapnik&amp;marker=51.5075%2C-0.1258"
    title="Map showing the location of Green Leaf Cafe in central London"
    loading="lazy">
  </iframe>
  <p><a href="https://www.openstreetmap.org/?mlat=51.5075&amp;mlon=-0.1258#map=17/51.5075/-0.1258">Open a larger map</a></p>
</section>

You will see the address, clickable phone and email links, and a map with a marker. On a phone, tapping the phone number starts a call.

Tip: Never put an important address only inside a map. Some users can't see or use the map. Writing the address as normal text makes it accessible, and search engines can read it too.

The complete contact page code

Here is the whole page with all four steps put together. Copy it, save it as contact.html and open it in your browser.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contact Us - Green Leaf Cafe</title>
  <meta name="description" content="Contact Green Leaf Cafe in London. Send us a message, see our opening hours and find us on the map.">
  <link rel="stylesheet" href="css/styles.css">
  <link rel="icon" href="images/favicon.svg" type="image/svg+xml">
</head>
<body>
  <header>
    <p><a href="/">Green Leaf Cafe</a></p>
    <nav aria-label="Main">
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/menu">Menu</a></li>
        <li><a href="/contact" aria-current="page">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Contact us</h1>
    <p>Questions, bookings or feedback? We would love to hear from you.</p>

    <section aria-labelledby="form-heading">
      <h2 id="form-heading">Send us a message</h2>
      <p>Fields marked * are required.</p>

      <form action="/contact" method="post">
        <p>
          <label for="name">Your name *</label><br>
          <input type="text" id="name" name="name" required minlength="2" maxlength="80" autocomplete="name">
        </p>

        <p>
          <label for="email">Email address *</label><br>
          <input type="email" id="email" name="email" required autocomplete="email">
        </p>

        <p>
          <label for="phone">Phone number (optional)</label><br>
          <input type="tel" id="phone" name="phone" autocomplete="tel">
        </p>

        <fieldset>
          <legend>What is your message about? *</legend>
          <p>
            <input type="radio" id="reason-booking" name="reason" value="booking" required>
            <label for="reason-booking">Table booking</label>
          </p>
          <p>
            <input type="radio" id="reason-event" name="reason" value="event">
            <label for="reason-event">Private event</label>
          </p>
          <p>
            <input type="radio" id="reason-other" name="reason" value="other">
            <label for="reason-other">Something else</label>
          </p>
        </fieldset>

        <p>
          <label for="message">Message *</label><br>
          <textarea id="message" name="message" rows="6" required minlength="10" maxlength="1000"></textarea>
        </p>

        <p>
          <input type="checkbox" id="privacy" name="privacy" value="agreed" required>
          <label for="privacy">I agree that Green Leaf Cafe can use my details to reply to me.</label>
        </p>

        <p>
          <button type="submit">Send message</button>
        </p>
      </form>
    </section>

    <section aria-labelledby="hours-heading">
      <h2 id="hours-heading">Opening hours</h2>
      <table>
        <caption>Green Leaf Cafe opening hours</caption>
        <thead>
          <tr>
            <th scope="col">Day</th>
            <th scope="col">Opens</th>
            <th scope="col">Closes</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <th scope="row">Monday</th>
            <td colspan="2">Closed</td>
          </tr>
          <tr>
            <th scope="row">Tuesday – Friday</th>
            <td><time>08:00</time></td>
            <td><time>18:00</time></td>
          </tr>
          <tr>
            <th scope="row">Saturday</th>
            <td><time>09:00</time></td>
            <td><time>20:00</time></td>
          </tr>
          <tr>
            <th scope="row">Sunday</th>
            <td><time>10:00</time></td>
            <td><time>16:00</time></td>
          </tr>
        </tbody>
      </table>
    </section>

    <section aria-labelledby="find-heading">
      <h2 id="find-heading">Find us</h2>
      <address>
        Green Leaf Cafe<br>
        12 Example Street<br>
        London WC2N 5DN<br>
        Phone: <a href="tel:+442079460000">020 7946 0000</a><br>
        Email: <a href="mailto:hello@greenleafcafe.example">hello@greenleafcafe.example</a>
      </address>

      <iframe
        width="600"
        height="400"
        src="https://www.openstreetmap.org/export/embed.html?bbox=-0.1298%2C51.5055%2C-0.1218%2C51.5095&amp;layer=mapnik&amp;marker=51.5075%2C-0.1258"
        title="Map showing the location of Green Leaf Cafe in central London"
        loading="lazy">
      </iframe>
      <p><a href="https://www.openstreetmap.org/?mlat=51.5075&amp;mlon=-0.1258#map=17/51.5075/-0.1258">Open a larger map</a></p>
    </section>
  </main>

  <footer>
    <p>© 2026 Green Leaf Cafe</p>
    <p><a href="/privacy">Privacy policy</a></p>
  </footer>
</body>
</html>

Read through the final page from top to bottom. Every part has a clear job, and every field and frame has a label or title. Test it with only your keyboard: press Tab to move through the links and fields, Space to tick boxes, and Enter to submit. If you can do everything without a mouse, your HTML is in good shape.

Common mistake: Copying the same id twice when you build a page in parts. Every id must be unique on the page, or labels will connect to the wrong fields. Check name, email and the heading ids if you add more sections.

Practice exercise

Make the page your own. Change the cafe into a business you like (a bike shop, a hair salon, a dentist), update all the text, opening hours and map location, and add one extra form field, for example a <select> for "Preferred contact method" with email and phone options.

Hint: To change the map, find your location on openstreetmap.org, click Share, and copy the new HTML code into your iframe. Keep the title and loading="lazy".

Key takeaways

  • A real page combines a good head, semantic landmarks, forms, tables and embeds.
  • Use the right input types, labels and validation attributes for a friendly contact form.
  • Opening hours are tabular data, so a table with a caption and headers is the right choice.
  • Always write the address as text too, not only inside a map.
  • Test your page with the keyboard only to catch accessibility problems early.