Construction — Contact & Quote
A rugged contact section for a construction firm with a request-a-callback quote form featuring inline validation, a service-area map placeholder, business hours with a live open or closed status indicator, and licensed plus insured trust badges.
MCP
Code
:root {
--hv: #f5c518;
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6;
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a;
--white: #ffffff;
--line: #d6d3cc;
--ok: #2f9e44;
--err: #d6391f;
--radius: 10px;
--shadow: 0 2px 0 rgba(27, 34, 43, .9), 0 10px 26px rgba(27, 34, 43, .14);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: var(--font);
color: var(--ink);
background: var(--bg)
repeating-linear-gradient(135deg, transparent 0 22px, rgba(43, 52, 64, .025) 22px 24px);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
padding: 32px 16px;
}
.contact { max-width: 980px; margin: 0 auto; }
/* ---- header ---- */
.contact__head { margin-bottom: 22px; }
.kicker {
display: inline-block;
font-size: .72rem;
font-weight: 800;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--steel-d);
background: var(--hv);
padding: 5px 12px;
border: 2px solid var(--steel-d);
border-radius: 3px;
}
.contact__head h1 {
font-size: clamp(2rem, 6vw, 3rem);
font-weight: 800;
letter-spacing: -.02em;
text-transform: uppercase;
margin: 12px 0 6px;
}
.lede { color: var(--muted); max-width: 52ch; margin: 0; font-size: 1rem; }
/* ---- layout ---- */
.grid {
display: grid;
grid-template-columns: 1.25fr 1fr;
gap: 20px;
align-items: start;
}
.side { display: grid; gap: 20px; }
/* ---- card ---- */
.card {
background: var(--white);
border: 2px solid var(--steel-d);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 22px;
}
.card__title {
font-size: 1.05rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .03em;
margin: 0 0 16px;
padding-bottom: 10px;
border-bottom: 3px solid var(--hv);
display: inline-block;
}
/* ---- form ---- */
.field { margin-bottom: 16px; }
.field label {
display: block;
font-size: .82rem;
font-weight: 700;
letter-spacing: .02em;
margin-bottom: 6px;
text-transform: uppercase;
}
.opt { color: var(--muted); font-weight: 500; text-transform: none; }
.field input,
.field select,
.field textarea {
width: 100%;
font-family: inherit;
font-size: .95rem;
color: var(--ink);
background: #f6f5f2;
border: 2px solid var(--line);
border-radius: 7px;
padding: 11px 12px;
transition: border-color .15s, box-shadow .15s, background .15s;
}
.field textarea { resize: vertical; min-height: 76px; }
.field input::placeholder,
.field textarea::placeholder { color: #9aa0a8; }
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
background: var(--white);
border-color: var(--steel);
box-shadow: 0 0 0 3px rgba(245, 197, 24, .45);
}
.field.invalid input,
.field.invalid select {
border-color: var(--err);
background: #fdf1ef;
}
.field.invalid input:focus,
.field.invalid select:focus {
box-shadow: 0 0 0 3px rgba(214, 57, 31, .25);
}
.error {
color: var(--err);
font-size: .78rem;
font-weight: 600;
margin: 6px 0 0;
min-height: 0;
display: none;
}
.field.invalid .error { display: block; }
/* ---- button ---- */
.btn {
font-family: inherit;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .04em;
cursor: pointer;
border-radius: 8px;
transition: transform .08s, box-shadow .15s, background .15s;
}
.btn--cta {
width: 100%;
font-size: 1rem;
color: var(--steel-d);
background: var(--hv);
border: 2px solid var(--steel-d);
padding: 14px;
box-shadow: 0 4px 0 var(--steel-d);
}
.btn--cta:hover { background: #ffd429; }
.btn--cta:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--steel-d); }
.btn--cta:focus-visible { outline: 3px solid var(--orange); outline-offset: 3px; }
.form-note { color: var(--muted); font-size: .78rem; text-align: center; margin: 10px 0 0; }
/* ---- success ---- */
.success {
margin-top: 16px;
display: flex;
align-items: center;
gap: 12px;
background: #ecf7ee;
border: 2px solid var(--ok);
border-radius: 8px;
padding: 14px 16px;
}
.success__check {
flex: none;
width: 30px; height: 30px;
display: grid; place-items: center;
background: var(--ok);
color: #fff;
border-radius: 50%;
font-weight: 800;
}
.success strong { display: block; }
.success span { color: #2c5733; font-size: .9rem; }
/* ---- map ---- */
.map {
position: relative;
height: 168px;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--steel-d);
background: #dfe3e0;
}
.map__grid {
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(43, 52, 64, .12) 1px, transparent 1px),
linear-gradient(90deg, rgba(43, 52, 64, .12) 1px, transparent 1px);
background-size: 26px 26px;
}
.map__radius {
position: absolute;
top: 50%; left: 50%;
width: 150px; height: 150px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: rgba(232, 100, 42, .14);
border: 2px dashed var(--orange);
}
.map__pin {
position: absolute;
top: 50%; left: 50%;
width: 16px; height: 16px;
transform: translate(-50%, -50%);
background: var(--orange);
border: 3px solid var(--white);
border-radius: 50% 50% 50% 0;
rotate: 45deg;
box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
}
.map__label {
position: absolute;
bottom: 10px; left: 10px;
background: var(--steel-d);
color: var(--hv);
font-size: .72rem;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 4px;
}
.map-note { color: var(--muted); font-size: .85rem; margin: 12px 0 0; }
.map-note strong { color: var(--ink); }
/* ---- hours ---- */
.hours-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.hours-head .card__title { margin-bottom: 0; }
.status {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: .76rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .04em;
padding: 5px 11px;
border-radius: 999px;
border: 2px solid var(--line);
color: var(--muted);
background: #f3f2ef;
}
.status__dot {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--muted);
}
.status.is-open { color: #1d6b2e; background: #e7f6ea; border-color: var(--ok); }
.status.is-open .status__dot { background: var(--ok); box-shadow: 0 0 0 3px rgba(47, 158, 68, .25); }
.status.is-closed { color: #a32d18; background: #fbeae6; border-color: var(--err); }
.status.is-closed .status__dot { background: var(--err); }
.hours { width: 100%; border-collapse: collapse; }
.hours th, .hours td {
text-align: left;
padding: 8px 4px;
font-size: .9rem;
border-bottom: 1px solid var(--line);
}
.hours th { font-weight: 700; width: 64px; }
.hours td { color: var(--muted); }
.hours tr:last-child th,
.hours tr:last-child td { border-bottom: none; }
.hours tr.today th,
.hours tr.today td {
background: rgba(245, 197, 24, .18);
color: var(--ink);
font-weight: 700;
}
.hours .closed { color: var(--err); font-weight: 600; }
.phone-line { font-size: .9rem; margin: 14px 0 0; color: var(--muted); }
.phone-line a { color: var(--orange); font-weight: 800; text-decoration: none; }
.phone-line a:hover { text-decoration: underline; }
/* ---- badges ---- */
.badges { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.badge {
display: flex;
align-items: center;
gap: 12px;
background: var(--steel);
color: var(--white);
border-radius: 8px;
padding: 12px 14px;
border-left: 5px solid var(--hv);
}
.badge__icon {
flex: none;
width: 38px; height: 38px;
display: grid; place-items: center;
background: var(--steel-d);
border-radius: 7px;
font-size: 1.15rem;
}
.badge strong { display: block; font-size: .92rem; }
.badge span { font-size: .78rem; color: #aeb6c0; }
/* ---- responsive ---- */
@media (max-width: 760px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
body { padding: 20px 12px; }
.card { padding: 18px; }
.contact__head h1 { font-size: 2rem; }
.hours-head { flex-direction: column; align-items: flex-start; }
}(function () {
"use strict";
/* ---------- Business hours / open-closed status ---------- */
// dayOfWeek (0=Sun) -> { open: minutes, close: minutes } or null when closed
var SCHEDULE = {
0: null, // Sun
1: { open: 420, close: 1020 }, // Mon 7:00–17:00
2: { open: 420, close: 1020 },
3: { open: 420, close: 1020 },
4: { open: 420, close: 1020 },
5: { open: 420, close: 960 }, // Fri 7:00–16:00
6: { open: 480, close: 720 } // Sat 8:00–12:00
};
function updateStatus() {
var statusEl = document.getElementById("status");
var textEl = document.getElementById("statusText");
if (!statusEl || !textEl) return;
var now = new Date();
var day = now.getDay();
var mins = now.getHours() * 60 + now.getMinutes();
var today = SCHEDULE[day];
var isOpen = !!today && mins >= today.open && mins < today.close;
statusEl.classList.remove("is-open", "is-closed");
statusEl.classList.add(isOpen ? "is-open" : "is-closed");
textEl.textContent = isOpen ? "Open now" : "Closed";
// Highlight today's row
var rows = document.querySelectorAll("#hoursTable tr");
rows.forEach(function (r) {
r.classList.toggle("today", Number(r.getAttribute("data-day")) === day);
});
}
/* ---------- Phone formatting ---------- */
function formatPhone(value) {
var d = value.replace(/\D/g, "").slice(0, 10);
if (d.length === 0) return "";
if (d.length < 4) return "(" + d;
if (d.length < 7) return "(" + d.slice(0, 3) + ") " + d.slice(3);
return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6);
}
/* ---------- Validation ---------- */
function setError(field, msg) {
var wrap = field.closest(".field");
var errEl = wrap ? wrap.querySelector(".error") : null;
if (msg) {
wrap.classList.add("invalid");
if (errEl) errEl.textContent = msg;
field.setAttribute("aria-invalid", "true");
} else {
wrap.classList.remove("invalid");
if (errEl) errEl.textContent = "";
field.removeAttribute("aria-invalid");
}
}
function validateField(field) {
var v = field.value.trim();
switch (field.id) {
case "name":
if (!v) return setError(field, "Please enter your name."), false;
if (v.length < 2) return setError(field, "That name looks too short."), false;
break;
case "phone":
var digits = v.replace(/\D/g, "");
if (!digits) return setError(field, "Please enter a phone number."), false;
if (digits.length !== 10) return setError(field, "Enter a 10-digit phone number."), false;
break;
case "project":
if (!v) return setError(field, "Pick a project type."), false;
break;
case "address":
if (!v) return setError(field, "Where's the job site?"), false;
if (v.length < 5) return setError(field, "Add a bit more detail."), false;
break;
}
setError(field, "");
return true;
}
/* ---------- Wire up ---------- */
document.addEventListener("DOMContentLoaded", function () {
updateStatus();
setInterval(updateStatus, 60000);
var form = document.getElementById("quoteForm");
if (!form) return;
var phone = document.getElementById("phone");
if (phone) {
phone.addEventListener("input", function () {
var pos = phone.selectionStart;
var before = phone.value.length;
phone.value = formatPhone(phone.value);
// keep caret roughly in place when appending at end
if (pos === before) phone.selectionStart = phone.selectionEnd = phone.value.length;
if (phone.closest(".field").classList.contains("invalid")) validateField(phone);
});
}
var required = ["name", "phone", "project", "address"];
required.forEach(function (id) {
var el = document.getElementById(id);
if (!el) return;
el.addEventListener("blur", function () { validateField(el); });
el.addEventListener("change", function () {
if (el.closest(".field").classList.contains("invalid")) validateField(el);
});
});
form.addEventListener("submit", function (e) {
e.preventDefault();
var ok = true;
var firstBad = null;
required.forEach(function (id) {
var el = document.getElementById(id);
if (el && !validateField(el)) {
ok = false;
if (!firstBad) firstBad = el;
}
});
if (!ok) {
if (firstBad) firstBad.focus();
return;
}
var name = document.getElementById("name").value.trim().split(" ")[0];
var success = document.getElementById("success");
var msg = document.getElementById("successMsg");
if (msg) {
msg.textContent = "Thanks" + (name ? ", " + name : "") +
" — a project manager will call you back within one business day.";
}
if (success) success.hidden = false;
form.reset();
if (success) success.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ironline Construction — Contact & Quote</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="contact" aria-labelledby="contact-title">
<header class="contact__head">
<span class="kicker">Free on-site estimate</span>
<h1 id="contact-title">Get a Quote</h1>
<p class="lede">Tell us about the job and a project manager from Ironline Construction will
call you back — usually within one business day.</p>
</header>
<div class="grid">
<!-- FORM -->
<section class="card form-card" aria-labelledby="form-title">
<h2 id="form-title" class="card__title">Request a callback</h2>
<form id="quoteForm" novalidate>
<div class="field">
<label for="name">Full name</label>
<input type="text" id="name" name="name" autocomplete="name" placeholder="e.g. Marcus Delgado" />
<p class="error" data-for="name"></p>
</div>
<div class="field">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" inputmode="tel" autocomplete="tel" placeholder="(555) 000-0000" maxlength="14" />
<p class="error" data-for="phone"></p>
</div>
<div class="field">
<label for="project">Project type</label>
<select id="project" name="project">
<option value="">Select a project type…</option>
<option>Kitchen remodel</option>
<option>Bathroom remodel</option>
<option>Home addition</option>
<option>Roofing & siding</option>
<option>Deck or patio</option>
<option>New construction</option>
<option>Commercial build-out</option>
<option>Other</option>
</select>
<p class="error" data-for="project"></p>
</div>
<div class="field">
<label for="address">Job-site address</label>
<input type="text" id="address" name="address" autocomplete="street-address" placeholder="142 Foundry Rd, Northgate" />
<p class="error" data-for="address"></p>
</div>
<div class="field">
<label for="message">Project details <span class="opt">(optional)</span></label>
<textarea id="message" name="message" rows="3" placeholder="Scope, square footage, timeline, budget range…"></textarea>
</div>
<button type="submit" class="btn btn--cta">Request my callback</button>
<p class="form-note">No spam. We only call about your project.</p>
</form>
<div class="success" id="success" role="status" hidden>
<span class="success__check" aria-hidden="true">✓</span>
<div>
<strong>Request received.</strong>
<span id="successMsg">Thanks — we'll call you back shortly.</span>
</div>
</div>
</section>
<!-- SIDEBAR -->
<aside class="side">
<!-- MAP -->
<section class="card map-card" aria-labelledby="map-title">
<h2 id="map-title" class="card__title">Service area</h2>
<div class="map" role="img" aria-label="Map showing a 35 mile service radius around Northgate Yard">
<div class="map__grid" aria-hidden="true"></div>
<span class="map__pin" aria-hidden="true"></span>
<span class="map__radius" aria-hidden="true"></span>
<span class="map__label">Northgate Yard</span>
</div>
<p class="map-note">Serving Northgate & surrounding counties — <strong>35 mi radius</strong>.</p>
</section>
<!-- HOURS -->
<section class="card hours-card" aria-labelledby="hours-title">
<div class="hours-head">
<h2 id="hours-title" class="card__title">Business hours</h2>
<span class="status" id="status" aria-live="polite">
<span class="status__dot" aria-hidden="true"></span>
<span id="statusText">Checking…</span>
</span>
</div>
<table class="hours" id="hoursTable">
<tbody>
<tr data-day="1"><th scope="row">Mon</th><td>7:00 AM – 5:00 PM</td></tr>
<tr data-day="2"><th scope="row">Tue</th><td>7:00 AM – 5:00 PM</td></tr>
<tr data-day="3"><th scope="row">Wed</th><td>7:00 AM – 5:00 PM</td></tr>
<tr data-day="4"><th scope="row">Thu</th><td>7:00 AM – 5:00 PM</td></tr>
<tr data-day="5"><th scope="row">Fri</th><td>7:00 AM – 4:00 PM</td></tr>
<tr data-day="6"><th scope="row">Sat</th><td>8:00 AM – 12:00 PM</td></tr>
<tr data-day="0"><th scope="row">Sun</th><td class="closed">Closed</td></tr>
</tbody>
</table>
<p class="phone-line">Or call <a href="tel:+15550148820">(555) 014-8820</a></p>
</section>
<!-- BADGES -->
<section class="card badges-card" aria-label="Licensing and insurance">
<ul class="badges">
<li class="badge">
<span class="badge__icon" aria-hidden="true">⛑</span>
<div><strong>Licensed</strong><span>GC Lic. #CB-204781</span></div>
</li>
<li class="badge">
<span class="badge__icon" aria-hidden="true">🛡</span>
<div><strong>Insured & bonded</strong><span>$2M general liability</span></div>
</li>
<li class="badge">
<span class="badge__icon" aria-hidden="true">★</span>
<div><strong>Since 2004</strong><span>20+ years on the job</span></div>
</li>
</ul>
</section>
</aside>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Contact & Quote
A sturdy, hi-vis contact block for a general contractor. The left column is a request-a-callback form — name, phone, project type, job-site address and a short message — with inline validation that flags empty or malformed fields, formats the phone number as you type, and confirms the request with a success banner.
The right column carries the trust signals crews rely on: a service-area map placeholder marking the coverage radius, a business-hours table with a live Open now / Closed pill computed from the visitor’s clock, and licensed + insured badges with license numbers.
Everything is vanilla HTML, CSS and JS — no libraries, no build step. Drop it into any page and wire the form submit to your own backend or form service.
Illustrative UI only — not a real contractor or binding quote.