Section 4: Forms Without JavaScript
Every business website eventually needs to answer one question: how does a visitor become a lead? The contact form is the answer. It is the single most important conversion element on a business site — more important than the hero image, the testimonials, or the pricing table.
Earlier in section 2, we created a contact form on the contact page; here, we’ll enhance the form to include validation feedback and visual states, all handled purely by HTML attributes and CSS selectors.
We’ll also include a server-side process to handle form submission. For this, we’ll use a free third-party service like Formspree, Netlify, or Web3forms, which requires only a change to the action attribute, no backend code required.
By the end of this section, contact.html will have a full, working form with a two-column grid layout, a supporting business details panel, real-time HTML5 validation, focus states, and error styling — all in pure HTML and CSS.
Connecting the Form to a Backend Service
HTML forms submit data via HTTP. Without a server to receive that data, the form goes nowhere. For a static site (HTML + CSS only), the cleanest solution is a free third-party form handling service. You wire it up with a single change to the action attribute — no backend code, no servers to manage.
Formspree (Recommended for Beginners)
Formspree is a hosted form backend. Free tier allows 50 submissions/month. Setup takes under 2 minutes.
To set up Formspree, do the following:
1. Go to formspree.io and create a free account. Verify your registered email address.
2. Create a new form — Formspree gives you a unique endpoint URL like: https://formspree.io/f/xpwzknab
3. Set that URL as your form’s action attribute thus:
<form
action=”https://formspree.io/f/xpwzknab”
method=”POST”
>
4. Formspree reads the `name` attribute on each input
and emails you the submitted data automatically. No other configuration needed.
5. To redirect users after submission, add a hidden input:
<input type=”hidden” name=”_next” value=”https://yoursite.com/thank-you.html” />
When the user clicks “Send Message,” and the form passes HTML5 validation, the browser serialises all the field values (using their name attributes as keys) and sends a POST request to the service URL. The service receives it, sends you an email notification, and can redirect the user to a thank-you page. The entire flow is handled outside your HTML — your code stays static and simple.
Building the Complete Contact Form HTML
Now, copy this HTML code and replace the entire contents of the main section on the contact.html page.
<!-- The Full Code for contact.html in <main> Section -->
<!-- Page Banner -->
<section class="page-hero">
<h1>Get In Touch</h1>
<p>Tell us about your project. We'll get back to you within one business day.</p>
</section>
<!-- Contact Layout: Form + Info -->
<section class="contact-section">
<div class="contact-layout page-content">
<!-- LEFT: Contact Form -->
<div class="contact-form-wrapper">
<h2 class="contact-form-title">Send Us a Message</h2>
<p class="contact-form-desc">
Fill in the form below and a member of our team will be in contact shortly.
</p>
<form
class="contact-form"
action="https://formspree.io/f/YOUR_FORM_ID"
method="POST"
novalidate
>
<!-- Required field note -->
<p class="form-required-note">
Fields marked <span aria-hidden="true">*</span> are required.
</p>
<!-- Row 1: First Name + Last Name -->
<div class="form-row">
<div class="field-group">
<label for="first-name">
First Name <span aria-hidden="true">*</span>
</label>
<input
type="text"
id="first-name"
name="first-name"
placeholder="Jane"
required
minlength="2"
autocomplete="given-name"
/>
</div>
<div class="field-group">
<label for="last-name">
Last Name <span aria-hidden="true">*</span>
</label>
<input
type="text"
id="last-name"
name="last-name"
placeholder="Doe"
required
minlength="2"
autocomplete="family-name"
/>
</div>
</div>
<!-- Row 2: Email + Phone -->
<div class="form-row">
<div class="field-group">
<label for="email">
Email Address <span aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
placeholder="jane@example.com"
required
autocomplete="email"
/>
</div>
<div class="field-group">
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
name="phone"
placeholder="+234 800 000 0000"
pattern="[0-9+\-\s]{7,20}"
autocomplete="tel"
/>
<span class="field-hint">Optional — digits, spaces, + and - only</span>
</div>
</div>
<!-- Row 3: Company (full width) -->
<div class="field-group">
<label for="company">Company / Organisation</label>
<input
type="text"
id="company"
name="company"
placeholder="Acme Ltd (optional)"
autocomplete="organization"
/>
</div>
<!-- Row 4: Service Interest dropdown (full width) -->
<div class="field-group">
<label for="service">
Service of Interest <span aria-hidden="true">*</span>
</label>
<select id="service" name="service" required>
<option value="" disabled selected>Select a service...</option>
<option value="web-design">Web Design</option>
<option value="web-development">Web Development</option>
<option value="consulting">Consulting</option>
<option value="seo">SEO & Performance</option>
<option value="other">Other</option>
</select>
</div>
<!-- Row 5: Message textarea (full width) -->
<div class="field-group">
<label for="message">
Message <span aria-hidden="true">*</span>
</label>
<textarea
id="message"
name="message"
rows="5"
placeholder="Tell us about your project..."
required
minlength="20"
autocomplete="off"
></textarea>
<span class="field-hint">Minimum 20 characters</span>
</div>
<!-- Row 6: Consent checkbox -->
<div class="field-group field-group--checkbox">
<input
type="checkbox"
id="consent"
name="consent"
required
value="yes"
/>
<label for="consent">
I agree to being contacted about my enquiry.
<span aria-hidden="true">*</span>
</label>
</div>
<!-- Honeypot spam trap (hidden from humans, bots fill it in) -->
<input type="text" name="_honey" style="display:none" tabindex="-1" aria-hidden="true" />
<!-- Submit button -->
<button type="submit" class="submit-btn">
Send Message ➤
</button>
</form>
</div><!-- /.contact-form-wrapper -->
<!-- RIGHT: Business Contact Details -->
<aside class="contact-details" aria-label="Contact information">
<h2>Contact Details</h2>
<div class="contact-item">
<span class="contact-icon" aria-hidden="true">📍</span>
<div>
<p class="contact-item-label">Address</p>
<p class="contact-item-value">Old Umuahia, Umuahia,<br>Abia State, Nigeria</p>
</div>
</div>
<div class="contact-item">
<span class="contact-icon" aria-hidden="true">📞</span>
<div>
<p class="contact-item-label">Phone</p>
<a href="tel:+2348037321869" class="contact-item-value">+234 803 732 1869</a>
</div>
</div>
<div class="contact-item">
<span class="contact-icon" aria-hidden="true">📧</span>
<div>
<p class="contact-item-label">Email</p>
<a href="mailto:contact@kmacims.com.ng" class="contact-item-value">contact@kmacims.com.ng</a>
</div>
</div>
<div class="contact-item">
<span class="contact-icon" aria-hidden="true">🕐</span>
<div>
<p class="contact-item-label">Business Hours</p>
<p class="contact-item-value">Mon – Fri: 9am – 4.30pm WAT</p>
</div>
</div>
</aside>
</div><!-- /.contact-layout -->
</section>
Note the following:
The hidden _honey input is a simple spam prevention trick. It is invisible to real users (hidden with inline CSS, removed from tab order with tabindex=”-1″, and hidden from screen readers with aria-hidden). Automated bots crawl forms and fill every visible field — including hidden ones. Formspree and Netlify Forms both check for this: if _honey has a value, the submission is flagged as spam and discarded.
We added novalidate to the <form> element. This disables the browser’s native validation UI (the ugly, unstyled tooltips) while keeping all required, pattern, and minlength attributes active so that our CSS pseudo-classes still respond to validity state. The receiving service will still validate the form. If you prefer the native browser tooltips, simply remove novalidate.
Style the Form Layout Using CSS Grid
The contact page uses a two-column layout: the form on the left, business details on the right. On small screens, this collapses to a single column. Inside the form itself, the name and email/phone rows are also two-column grids that collapse gracefully on narrow viewports.
Copy and replace the existing styles for the contact page in css/styles.css.
/* Updated CONTACT PAGE Styles CSS */
/* Outer section wrapper */
.contact-section {
background: var(--bg-color);
}
/* Two-column page layout: form | details */
.contact-layout {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 4rem;
align-items: start;
}
/* Stack on mobile */
@media (max-width: 768px) {
.contact-layout {
grid-template-columns: 1fr;
gap: 2.5rem;
}
}
/* Form section heading */
.contact-form-title {
font-size: clamp(1.35rem, 2.5vw, 1.75rem);
font-weight: 700;
margin-bottom: 0.4rem;
color: var(--text-color);
}
.contact-form-desc {
font-size: 0.95rem;
color: #4b5563;
margin-bottom: 2rem;
line-height: 1.65;
}
/* Required field note */
.form-required-note {
font-size: 0.82rem;
color: #6b7280;
margin-bottom: 1.5rem;
}
/* The form itself — stacked grid */
.contact-form {
display: grid;
gap: 1.25rem;
}
/* Two-column row inside the form */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
/* Collapse form rows on small screens */
@media (max-width: 520px) {
.form-row {
grid-template-columns: 1fr;
}
}
/* Individual field wrapper */
.field-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.field-group label {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-color);
}
/* Required asterisk */
.field-group label span[aria-hidden] {
color: #ef4444;
margin-left: 2px;
}
/* Shared input / select / textarea styles */
.field-group input,
.field-group select,
.field-group textarea {
font-family: system-ui, sans-serif;
font-size: 0.95rem;
padding: 0.65rem 0.875rem;
border: 1.5px solid #d1d5db;
border-radius: 7px;
color: var(--text-color);
background: white;
width: 100%;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
outline: none;
}
.field-group textarea {
resize: vertical;
min-height: 120px;
line-height: 1.6;
}
/* Hint text beneath a field */
.field-hint {
font-size: 0.78rem;
color: #9ca3af;
}
/* Checkbox row layout */
.field-group--checkbox {
flex-direction: row;
align-items: flex-start;
gap: 0.6rem;
}
.field-group--checkbox input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
flex-shrink: 0;
margin-top: 0.15rem;
accent-color: var(--primary-color);
cursor: pointer;
}
.field-group--checkbox label {
font-size: 0.88rem;
font-weight: 400;
color: #4b5563;
cursor: pointer;
}
/* Focus State */
.field-group input:focus,
.field-group select:focus,
.field-group textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
outline: none;
}
/* Valid state — green border after user types */
.field-group input:valid:not(:placeholder-shown),
.field-group textarea:valid:not(:placeholder-shown) {
border-color: #22c55e;
}
/* Invalid state — red border after user types */
.field-group input:invalid:not(:placeholder-shown),
.field-group textarea:invalid:not(:placeholder-shown) {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Hint text turns red when field is invalid */
.field-group input:invalid:not(:placeholder-shown) ~ .field-hint,
.field-group textarea:invalid:not(:placeholder-shown) ~ .field-hint {
color: #ef4444;
}
/* Submit Button */
.submit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: var(--primary-color);
color: white;
font-family: system-ui, sans-serif;
font-size: 1rem;
font-weight: 600;
padding: 0.85rem 2rem;
border: none;
border-radius: 8px;
cursor: pointer;
width: 100%;
margin-top: 0.5rem;
transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}
.submit-btn:hover {
background: #1d4ed8;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3);
}
.submit-btn:active {
transform: translateY(0);
box-shadow: none;
}
/* Contact Details Sidebar */
.contact-details {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 2rem;
position: sticky;
top: 2rem;
}
.contact-details h2 {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--text-color);
}
.contact-item {
display: flex;
align-items: flex-start;
gap: 0.85rem;
margin-bottom: 1.25rem;
}
.contact-icon {
font-size: 1.2rem;
flex-shrink: 0;
color: var(--primary-color);
margin-top: 0.1rem;
}
.contact-item-label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #9ca3af;
margin-bottom: 0.2rem;
}
.contact-item-value {
font-size: 0.92rem;
color: var(--text-color);
font-weight: 500;
text-decoration: none;
}
a.contact-item-value:hover {
color: var(--primary-color);
text-decoration: underline;
}
Adding position: sticky; top: 2rem to the contact details sidebar means it follows the user as they scroll down the page, filling in the long form — keeping the business phone number and email always in view. This is a subtle but professional UX touch that requires zero JavaScript.

