Project

Build a booking form with a local review step

Build a labeled booking form that validates native constraints and displays a safe local summary without sending data.

Advanced13 min readLearn · Practice · Review

Published by HTML Code Runner · AI-assisted material · Independent editorial review pending · Editorial policy

On this page

Project brief

A workshop organizer wants learners to review their choices before a booking would be submitted. Build the front-end review interaction with a name, email, session choice, ticket count, and optional access note. The completed demonstration must clearly state that it does not send a reservation or save personal information.

The important challenge is state, not visual decoration. Invalid values should receive native feedback. Valid values should produce a readable summary. Editing the fields and reviewing again should replace the previous summary rather than duplicating it. User-entered text must remain text even if it contains characters that resemble HTML.

Use invented information while testing. The project is a practice interface, not a production booking service. A real service would need a receiving endpoint, server validation, capacity checks, authorization where relevant, privacy handling, and a genuine confirmation that the reservation exists.

Learning objectives

Combine semantic HTML, accessible controls, clear reading order, and deliberate browser behavior in a complete small interface. Explain each structural decision, then verify the result with the acceptance checks below.

Prerequisites

Complete the linked lessons before starting. Use a text editor and a browser with developer tools. The provided solution runs entirely in the browser and uses no external libraries or assets.

Design and implementation decisions

The form relies on native required, email, number, minimum, maximum, and radio-group behavior before its submit event runs. The script prevents network submission and creates a local summary using DOM nodes and textContent. It does not concatenate user values into innerHTML.

The summary is a named section outside the form. A short status region announces that the review was updated without reading every entered value aloud. The script does not move focus unexpectedly, allowing the person to continue from the Review choices button and inspect the summary in normal document order.

The session choices use fieldset and legend so the group question is available alongside each option. The access note remains optional and explains its purpose. Keeping that field optional avoids making people disclose information they do not need to share for the demonstration.

Build it step by step

  1. Build visible labels and unique IDs for every field.
  2. Group the session radios with a fieldset and legend.
  3. Add constraints for a ticket count from one to four.
  4. Listen for the form submit event and prevent its default network action.
  5. Read FormData, then build summary nodes with textContent.
  6. Update a short status message and test repeated reviews.

Runnable solution

HTML structure

<a class="skip" href="#main">Skip to booking practice</a><header><p><strong>Northside Workshops</strong></p></header><main id="main"><h1>Review your workshop choices</h1><p>This practice form creates a local summary only. It does not make a reservation or send your details.</p><form id="booking-form"><label for="guest-name">Name</label><input id="guest-name" name="name" autocomplete="name" required><label for="guest-email">Email address</label><input id="guest-email" name="email" type="email" autocomplete="email" required><fieldset><legend>Workshop session</legend><label><input type="radio" name="session" value="Morning HTML workshop" required> Morning HTML workshop</label><label><input type="radio" name="session" value="Afternoon accessibility workshop"> Afternoon accessibility workshop</label></fieldset><label for="ticket-count">Tickets (1 to 4)</label><input id="ticket-count" name="tickets" type="number" min="1" max="4" step="1" value="1" required><label for="access-note">Access note (optional)</label><textarea id="access-note" name="note" rows="3" aria-describedby="note-help"></textarea><p id="note-help">Use fictional information in this learning example.</p><button type="submit">Review choices</button><p id="review-status" role="status"></p></form><section id="review" aria-labelledby="review-title" hidden><h2 id="review-title">Your local review</h2><dl id="review-values"></dl><p>No reservation has been sent.</p></section></main>

Styles

body{margin:0;background:#f5f8f6;color:#1c302a;font:16px/1.65 system-ui,sans-serif}main,header,footer{max-width:940px;margin:auto;padding:24px}h1{font-size:clamp(2rem,5vw,3.5rem);line-height:1.12}h2{line-height:1.3}a{color:#12644d;text-underline-offset:3px}button,input,select,textarea{font:inherit}button{cursor:pointer;background:#12644d;color:white;border:0;padding:10px 16px;border-radius:6px}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid #bc6f00;outline-offset:3px}section,article,fieldset{background:white;border:1px solid #d7e5de;padding:20px;border-radius:10px;margin:20px 0}label{display:block;margin-top:12px}input,select,textarea{padding:8px;border:1px solid #73887e;border-radius:4px;max-width:100%;box-sizing:border-box}table{border-collapse:collapse;width:100%}th,td{border:1px solid #c8d7cf;text-align:left;padding:10px}caption{text-align:left;font-weight:700;margin-bottom:10px}nav{display:flex;flex-wrap:wrap;gap:16px}summary{cursor:pointer;font-weight:650;padding:8px}footer{color:#4e665b}.skip{position:absolute;top:-100px}.skip:focus{top:0;background:white;padding:8px;z-index:10}form{max-width:620px}input:not([type=radio]),textarea{width:100%}fieldset label{margin:8px 0}dt{font-weight:700}dd{margin:0 0 12px;overflow-wrap:anywhere}

Behavior

const form = document.querySelector('#booking-form');
form.addEventListener('submit', event => {
  event.preventDefault();
  const data = new FormData(form);
  const values = document.querySelector('#review-values');
  values.replaceChildren();
  for (const [key, label] of [['name','Name'],['email','Email'],['session','Session'],['tickets','Tickets'],['note','Access note']]) {
    const term = document.createElement('dt'); term.textContent = label;
    const description = document.createElement('dd'); description.textContent = String(data.get(key) || 'Not supplied');
    values.append(term, description);
  }
  document.querySelector('#review').hidden = false;
  document.querySelector('#review-status').textContent = 'Your local review is ready below. No details were sent.';
});
Complete standalone HTML file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Build a booking form with a local review step</title>
<style>body{margin:0;background:#f5f8f6;color:#1c302a;font:16px/1.65 system-ui,sans-serif}main,header,footer{max-width:940px;margin:auto;padding:24px}h1{font-size:clamp(2rem,5vw,3.5rem);line-height:1.12}h2{line-height:1.3}a{color:#12644d;text-underline-offset:3px}button,input,select,textarea{font:inherit}button{cursor:pointer;background:#12644d;color:white;border:0;padding:10px 16px;border-radius:6px}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid #bc6f00;outline-offset:3px}section,article,fieldset{background:white;border:1px solid #d7e5de;padding:20px;border-radius:10px;margin:20px 0}label{display:block;margin-top:12px}input,select,textarea{padding:8px;border:1px solid #73887e;border-radius:4px;max-width:100%;box-sizing:border-box}table{border-collapse:collapse;width:100%}th,td{border:1px solid #c8d7cf;text-align:left;padding:10px}caption{text-align:left;font-weight:700;margin-bottom:10px}nav{display:flex;flex-wrap:wrap;gap:16px}summary{cursor:pointer;font-weight:650;padding:8px}footer{color:#4e665b}.skip{position:absolute;top:-100px}.skip:focus{top:0;background:white;padding:8px;z-index:10}form{max-width:620px}input:not([type=radio]),textarea{width:100%}fieldset label{margin:8px 0}dt{font-weight:700}dd{margin:0 0 12px;overflow-wrap:anywhere}</style>
</head>
<body>
<a class="skip" href="#main">Skip to booking practice</a><header><p><strong>Northside Workshops</strong></p></header><main id="main"><h1>Review your workshop choices</h1><p>This practice form creates a local summary only. It does not make a reservation or send your details.</p><form id="booking-form"><label for="guest-name">Name</label><input id="guest-name" name="name" autocomplete="name" required><label for="guest-email">Email address</label><input id="guest-email" name="email" type="email" autocomplete="email" required><fieldset><legend>Workshop session</legend><label><input type="radio" name="session" value="Morning HTML workshop" required> Morning HTML workshop</label><label><input type="radio" name="session" value="Afternoon accessibility workshop"> Afternoon accessibility workshop</label></fieldset><label for="ticket-count">Tickets (1 to 4)</label><input id="ticket-count" name="tickets" type="number" min="1" max="4" step="1" value="1" required><label for="access-note">Access note (optional)</label><textarea id="access-note" name="note" rows="3" aria-describedby="note-help"></textarea><p id="note-help">Use fictional information in this learning example.</p><button type="submit">Review choices</button><p id="review-status" role="status"></p></form><section id="review" aria-labelledby="review-title" hidden><h2 id="review-title">Your local review</h2><dl id="review-values"></dl><p>No reservation has been sent.</p></section></main>
<script>const form = document.querySelector('#booking-form');
form.addEventListener('submit', event => {
  event.preventDefault();
  const data = new FormData(form);
  const values = document.querySelector('#review-values');
  values.replaceChildren();
  for (const [key, label] of [['name','Name'],['email','Email'],['session','Session'],['tickets','Tickets'],['note','Access note']]) {
    const term = document.createElement('dt'); term.textContent = label;
    const description = document.createElement('dd'); description.textContent = String(data.get(key) || 'Not supplied');
    values.append(term, description);
  }
  document.querySelector('#review').hidden = false;
  document.querySelector('#review-status').textContent = 'Your local review is ready below. No details were sent.';
});</script>
</body>
</html>

Expected output

Build a labeled booking form that validates native constraints and displays a safe local summary without sending data. The complete file above can be saved as index.html and opened locally. The embedded playground uses the same HTML, CSS, and JavaScript, with additional isolation restrictions.

Acceptance rubric

  • An empty required field prevents a review and receives native feedback.
  • Malformed email and ticket counts outside one to four fail the applicable browser constraints.
  • Exactly one session can be selected.
  • Entering <strong>Test</strong> as a name displays those characters as text, not bold markup.
  • A second valid review replaces the first summary.
  • The page makes no reservation claim and performs no network submission.
  • Keyboard focus remains visible and the result can be reached in normal reading order.

Common mistakes

Check the relationships in the parsed document rather than judging only the screenshot. Repeated IDs can disconnect labels, a button without an explicit type can submit unexpectedly, and a visual heading may use the wrong semantic level. Test the supplied interaction after changing its structure instead of assuming the JavaScript selectors still find the intended controls.

Accessibility and browser notes

Use Tab and Shift+Tab through the entire interface. Focus must remain visible, and every control must have a useful accessible name. Increase browser zoom and verify that text wraps without hiding actions. Native control appearance varies by browser. A polished screenshot is not evidence that the keyboard workflow or error recovery works; exercise both separately.

Take it further

Add an explicit Edit choices link that returns to the first field only when the user asks for it. Then design the server contract on paper: request fields, validation errors, capacity conflicts, and successful confirmation. Keep that design separate from this local demo until it has a real implementation.

For a more advanced exercise, preserve a non-sensitive draft only after explaining local storage and providing a clear reset control. Do not silently store the access note or other personal information just to make the demo seem more sophisticated.

Related lessons

Reference standards

WHATWG HTML Living Standard · WAI tutorials