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, andaccept. - Labels and why
for/idpairing 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
acceptattribute.
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
:invalidCSS 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.