Overview
A job application form is the natural second HTML project because it forces you to move past the two or three input types most tutorials stop at. A real application needs plain text, an email address, a dropdown of positions, a number for years of experience, a set of checkboxes for skills that aren't mutually exclusive, radio buttons for an employment type that is mutually exclusive, and a file upload for a resume — five genuinely different input patterns, each suited to a different kind of answer.
This tutorial builds that form field by field, and leans on HTML's built-in validation attributes — `required`, `minlength`, `pattern`, `min`, `max` — so the browser rejects obviously invalid input before a single line of JavaScript is written.
- A `<fieldset>`/`<legend>` grouped Personal Details section with text, email, and phone inputs.
- A Position dropdown (`<select>`) and a numeric Years of Experience field.
- A group of Skills checkboxes, where any number can be checked.
- A group of Employment Type radio buttons, where exactly one can be selected.
- A resume file upload restricted to PDF/DOC files, plus a cover letter textarea.
- A submit button that the browser won't activate until every required field passes validation.
Prerequisites
- Basic HTML structure and how to nest elements.
- The generic `<form>` element and the idea of name/value pairs being submitted.
- Attributes in general — you will use `type`, `name`, `id`, `required`, `min`, `max`, `pattern`, and `accept`.
- Labels and why `for`/`id` pairing matters (covered briefly here, in more depth in the Portfolio project).
Project Structure
The whole form lives in one file, `application.html`, inside a single `<form>` element. Related fields are grouped with `<fieldset>`, and each fieldset gets a `<legend>` as its visible, accessible title — this is the correct semantic pairing for "here is a named group of controls", and it is exactly what a paper application form does with section headers like "Personal Details" or "Employment Type". A `<div>` with a heading above it looks similar, but assistive tech does not automatically associate that heading with the fields below it the way it does with `<legend>`.
Every field uses the input type that matches its data — `email`, `tel`, `number`, `file` — rather than defaulting everything to `type="text"`. Correct types matter because browsers change their behavior based on them: `type="email"` validates the format, `type="number"` shows a numeric keypad on mobile and a spinner on desktop, and `type="file"` opens the operating system's native file picker.
Step 1: Scaffold the Page and Form Container
Start with the standard boilerplate and open the `<form>` itself. The `action`/`method` attributes describe where the data would be sent in a real deployment; here they are left as placeholders since this tutorial focuses on the markup.
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Job Application - Frontend Developer Role</title> <meta name="description" content="Apply for the Frontend Developer role using this application form."></head><body>
<main> <h1>Job Application: Frontend Developer</h1> <p>Fields marked with * are required.</p>
<!-- method="post" is correct here: the submitted data (personal details, a file) shouldn't be exposed in the URL the way method="get" would expose it --> <form action="#" method="post" enctype="multipart/form-data"> <!-- enctype="multipart/form-data" is required whenever a form includes a file input (added in Step 6) - without it, the file itself never gets sent -->
<!-- Fieldsets for each group of fields are added in the steps that follow -->
</form> </main>
</body></html>Step 2: Collect Personal Details
Personal details are grouped in a `<fieldset>`, with `<legend>` as its visible section title — the accessible equivalent of a bold heading over a group of paper form fields. Each field uses the input type suited to its data: `type="tel"` for a phone number hints a numeric keypad on mobile, and `pattern` constrains the phone number to a specific digit format without needing JavaScript.
<!-- Grouped with <fieldset>/<legend> rather than a <div> + heading: assistive tech automatically announces the legend as this group's name whenever any field inside it gets focus --><fieldset> <legend>Personal Details</legend>
<p> <label for="fullName">Full Name *</label><br> <input type="text" id="fullName" name="fullName" required minlength="2"> <!-- minlength blocks single-character "names" without any custom validation code --> </p>
<p> <label for="email">Email Address *</label><br> <input type="email" id="email" name="email" required> <!-- type="email" rejects obviously malformed addresses (e.g. missing @) before submit --> </p>
<p> <label for="phone">Phone Number *</label><br> <input type="tel" id="phone" name="phone" required pattern="[0-9]{10}" placeholder="10-digit number without spaces"> <!-- pattern enforces exactly 10 digits; placeholder tells the user the expected format --> </p></fieldset>Step 3: Add a Position Dropdown and Experience Field
Next comes the position dropdown and a numeric experience field. `<select>` is the right element whenever the user must choose exactly one option from a fixed, known list — it takes far less screen space than the equivalent radio button group would for five or more options.
<fieldset> <legend>Role Details</legend>
<p> <label for="position">Position Applying For *</label><br> <!-- <select> is best for one choice out of many known options; every browser gives it a native, keyboard-accessible dropdown for free --> <select id="position" name="position" required> <option value="">-- Select a position --</option> <!-- Empty value keeps this placeholder option from silently counting as a real answer --> <option value="frontend">Frontend Developer</option> <option value="backend">Backend Developer</option> <option value="fullstack">Full-Stack Developer</option> <option value="designer">UI/UX Designer</option> </select> </p>
<p> <label for="experience">Years of Experience *</label><br> <input type="number" id="experience" name="experience" required min="0" max="50" step="1"> <!-- type="number" + min/max keeps the value inside a realistic, valid range --> </p></fieldset>Step 4: Add Skill Checkboxes
Skills are checkboxes, not radio buttons, because more than one can genuinely be true at once — a candidate can know both HTML and CSS. Each checkbox needs its own unique `id`, but they can all share the same `name` so the server receives them as one group of selected values.
<fieldset> <legend>Skills (select all that apply)</legend>
<!-- Checkboxes, not radios: multiple skills can be true simultaneously, so the "pick any number" input type is the correct one here --> <p> <input type="checkbox" id="skillHtml" name="skills" value="HTML"> <label for="skillHtml">HTML</label> </p> <p> <input type="checkbox" id="skillCss" name="skills" value="CSS"> <label for="skillCss">CSS</label> </p> <p> <input type="checkbox" id="skillJs" name="skills" value="JavaScript"> <label for="skillJs">JavaScript</label> </p> <p> <input type="checkbox" id="skillReact" name="skills" value="React"> <label for="skillReact">React</label> </p> <!-- Every checkbox shares name="skills" so the server groups all checked values together --></fieldset>Step 5: Add Employment-Type Radio Buttons
Employment type is the opposite case — a candidate is either applying full-time, part-time, or as a contractor, never more than one at once — so radio buttons are correct here, not checkboxes. Sharing the same `name="employmentType"` across all three is what makes them mutually exclusive: selecting one automatically deselects the others.
<fieldset> <legend>Employment Type</legend>
<!-- Radio buttons, not checkboxes: exactly one employment type can apply, so only one option should ever be selectable at a time --> <p> <input type="radio" id="typeFull" name="employmentType" value="full-time" checked> <!-- checked marks a sensible default; the user can still pick a different option --> <label for="typeFull">Full-Time</label> </p> <p> <input type="radio" id="typePart" name="employmentType" value="part-time"> <label for="typePart">Part-Time</label> </p> <p> <input type="radio" id="typeContract" name="employmentType" value="contract"> <label for="typeContract">Contract</label> </p> <!-- All three share name="employmentType" - that shared name is what makes them mutually exclusive --></fieldset>Step 6: Add Resume Upload and Cover Letter
Resume upload needs `type="file"`, restricted with `accept` so the operating system's file picker filters to relevant formats by default. A cover letter is free-form and multi-line, so it gets a `<textarea>` instead of a single-line input.
<fieldset> <legend>Resume & Cover Letter</legend>
<p> <label for="resume">Upload Resume (PDF or DOC) *</label><br> <input type="file" id="resume" name="resume" accept=".pdf,.doc,.docx" required> <!-- accept hints the OS file picker to show only matching file types, though users can still override it --> </p>
<p> <label for="coverLetter">Cover Letter</label><br> <textarea id="coverLetter" name="coverLetter" rows="6" placeholder="Tell us why you're a good fit for this role..."></textarea> <!-- Not required: a cover letter is optional, unlike the resume above it --> </p></fieldset>Step 7: Add the Submit Button
Finally, the submit button. `type="submit"` is explicit rather than relying on `<button>`'s default, and it sits outside every `<fieldset>` since it applies to the whole form, not to one field group.
<!-- Sits outside every fieldset since it submits the whole form, not one group --><p> <button type="submit">Submit Application</button></p>Complete Code
Here is the full application form assembled in order, with a small stylesheet so every field group is easy to scan.
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Job Application - Frontend Developer Role</title> <meta name="description" content="Apply for the Frontend Developer role using this application form."> <style> * { box-sizing: border-box; } body { font-family: Arial, Helvetica, sans-serif; margin: 0; background: #f3f4f6; color: #222; line-height: 1.6; } main { max-width: 640px; margin: 0 auto; padding: 30px 20px; } h1 { color: #1d2940; } fieldset { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 16px 20px; margin-bottom: 20px; } legend { font-weight: bold; padding: 0 8px; color: #2b3a55; } label { display: block; font-weight: bold; margin-bottom: 4px; } input[type="text"], input[type="email"], input[type="tel"], input[type="number"], select, textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: inherit; font-size: 1rem; } input[type="checkbox"], input[type="radio"] { margin-right: 8px; } button { background: #2b3a55; color: #fff; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; font-size: 1rem; } button:hover { background: #1d2940; } </style></head><body>
<main> <h1>Job Application: Frontend Developer</h1> <p>Fields marked with * are required.</p>
<form action="#" method="post" enctype="multipart/form-data">
<!-- Named group of controls: <legend> is the accessible title for this fieldset --> <fieldset> <legend>Personal Details</legend> <p> <label for="fullName">Full Name *</label><br> <input type="text" id="fullName" name="fullName" required minlength="2"> </p> <p> <label for="email">Email Address *</label><br> <input type="email" id="email" name="email" required> </p> <p> <label for="phone">Phone Number *</label><br> <input type="tel" id="phone" name="phone" required pattern="[0-9]{10}" placeholder="10-digit number without spaces"> </p> </fieldset>
<fieldset> <legend>Role Details</legend> <p> <label for="position">Position Applying For *</label><br> <select id="position" name="position" required> <option value="">-- Select a position --</option> <option value="frontend">Frontend Developer</option> <option value="backend">Backend Developer</option> <option value="fullstack">Full-Stack Developer</option> <option value="designer">UI/UX Designer</option> </select> </p> <p> <label for="experience">Years of Experience *</label><br> <input type="number" id="experience" name="experience" required min="0" max="50" step="1"> </p> </fieldset>
<fieldset> <legend>Skills (select all that apply)</legend> <p> <input type="checkbox" id="skillHtml" name="skills" value="HTML"> <label for="skillHtml">HTML</label> </p> <p> <input type="checkbox" id="skillCss" name="skills" value="CSS"> <label for="skillCss">CSS</label> </p> <p> <input type="checkbox" id="skillJs" name="skills" value="JavaScript"> <label for="skillJs">JavaScript</label> </p> <p> <input type="checkbox" id="skillReact" name="skills" value="React"> <label for="skillReact">React</label> </p> </fieldset>
<fieldset> <legend>Employment Type</legend> <p> <input type="radio" id="typeFull" name="employmentType" value="full-time" checked> <label for="typeFull">Full-Time</label> </p> <p> <input type="radio" id="typePart" name="employmentType" value="part-time"> <label for="typePart">Part-Time</label> </p> <p> <input type="radio" id="typeContract" name="employmentType" value="contract"> <label for="typeContract">Contract</label> </p> </fieldset>
<fieldset> <legend>Resume & Cover Letter</legend> <p> <label for="resume">Upload Resume (PDF or DOC) *</label><br> <input type="file" id="resume" name="resume" accept=".pdf,.doc,.docx" required> </p> <p> <label for="coverLetter">Cover Letter</label><br> <textarea id="coverLetter" name="coverLetter" rows="6" placeholder="Tell us why you're a good fit for this role..."></textarea> </p> </fieldset>
<p> <button type="submit">Submit Application</button> </p> </form> </main>
</body></html>Sample Run
Open `application.html` in a browser, or use the live preview above, and try the following:
- Click Submit Application with the Full Name field empty — the browser stops the submission and points at that field itself, because of `required`, with zero JavaScript.
- Type a phone number with letters in it, like "abc1234567" — `pattern="[0-9]{10}"` rejects it on submit.
- Check two or more Skills boxes at once, then select an Employment Type — the skills stay independently checked while picking a new employment type automatically unchecks the previous one.
- Click "Upload Resume" — the operating system's native file picker opens, pre-filtered to PDF/DOC files because of the `accept` attribute.
Extend This Project
- Add a "Portfolio URL" field using `type="url"` so the browser validates it looks like a real link.
- Add a `<datalist>` of common job titles to the Position field so it behaves like an autocomplete text input instead of a fixed dropdown.
- Split Full Name into separate First Name / Last Name inputs and validate each with its own `pattern`.
- Add a required checkbox at the bottom ("I certify this information is accurate") before the submit button makes sense to click.
- Style invalid fields with the `:invalid` CSS pseudo-class so validation errors are visually obvious before the user even tries to submit.
Summary
You built a complete, multi-field job application form using the input type that actually matches each piece of data — text, email, tel, number, checkbox, radio, and file — instead of defaulting everything to a single generic text box. Combined with `required`, `pattern`, `min`, `max`, and `accept`, the browser now enforces meaningful validation on its own, which is the foundation every JavaScript-powered form validation library builds on top of.