Construction — Instant Estimate Request
A rugged trades estimate widget where project type, scope checkboxes, square footage, a finish-level slider and timeline drive a live ballpark price range, alongside validated contact fields and a request-detailed-quote submit that confirms the booking.
MCP
Code
:root {
--hv: #f5c518;
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6;
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a;
--white: #ffffff;
--line: #d6d3cc;
--radius: 6px;
--shadow: 0 8px 28px rgba(27, 34, 43, 0.14);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
padding: 32px 16px;
}
.estimate {
max-width: 960px;
margin: 0 auto;
background: var(--white);
border: 2px solid var(--steel-d);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
}
/* Header */
.est-head {
position: relative;
background: var(--steel);
color: var(--white);
}
.est-head__bar {
height: 8px;
background: repeating-linear-gradient(
-45deg,
var(--hv) 0 16px,
var(--steel-d) 16px 32px
);
}
.est-head__inner {
padding: 22px 26px 24px;
}
.est-kicker {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--hv);
}
#est-title {
font-size: clamp(1.5rem, 4vw, 2.1rem);
font-weight: 800;
letter-spacing: -0.02em;
text-transform: uppercase;
margin-top: 4px;
}
.est-sub {
color: #c3c8cf;
font-size: 0.92rem;
margin-top: 6px;
max-width: 46ch;
}
/* Grid */
.est-grid {
display: grid;
grid-template-columns: 1.25fr 1fr;
gap: 0;
}
.est-config {
padding: 24px 26px;
border-right: 1px solid var(--line);
}
/* Blocks */
.block {
border: none;
padding: 0;
margin-bottom: 22px;
}
.block legend {
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--steel);
padding: 0;
margin-bottom: 10px;
border-bottom: 3px solid var(--hv);
display: inline-block;
padding-bottom: 3px;
}
/* Segmented project type */
.seg {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.seg__opt input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.seg__opt span {
display: block;
padding: 9px 13px;
border: 1.5px solid var(--line);
border-radius: var(--radius);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
background: var(--bg);
transition: all 0.15s ease;
user-select: none;
}
.seg__opt span:hover {
border-color: var(--steel);
}
.seg__opt input:checked + span {
background: var(--steel);
border-color: var(--steel-d);
color: var(--white);
}
.seg__opt input:focus-visible + span {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
/* Checkboxes */
.checks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.check {
display: flex;
align-items: center;
gap: 9px;
padding: 10px 12px;
border: 1.5px solid var(--line);
border-radius: var(--radius);
font-size: 0.86rem;
font-weight: 500;
cursor: pointer;
background: var(--white);
transition: border-color 0.15s ease, background 0.15s ease;
}
.check:hover {
border-color: var(--steel);
}
.check input {
width: 17px;
height: 17px;
accent-color: var(--orange);
flex-shrink: 0;
cursor: pointer;
}
.check:has(input:checked) {
border-color: var(--orange);
background: #fdf1ea;
}
.check:has(input:focus-visible) {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
/* Fields */
.field {
margin-bottom: 16px;
}
.field label {
display: block;
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 6px;
color: var(--steel);
}
input[type="number"],
input[type="text"],
input[type="email"],
input[type="tel"],
select {
width: 100%;
padding: 10px 12px;
border: 1.5px solid var(--line);
border-radius: var(--radius);
font: inherit;
font-size: 0.92rem;
color: var(--ink);
background: var(--white);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus,
select:focus {
outline: none;
border-color: var(--steel);
box-shadow: 0 0 0 3px rgba(245, 197, 24, 0.4);
}
.sqft-row {
display: flex;
align-items: stretch;
gap: 8px;
}
.sqft-row input {
flex: 1;
}
.unit {
display: flex;
align-items: center;
padding: 0 14px;
background: var(--bg);
border: 1.5px solid var(--line);
border-radius: var(--radius);
font-weight: 700;
font-size: 0.85rem;
color: var(--muted);
}
/* Range slider */
input[type="range"] {
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 8px;
border-radius: 4px;
background: linear-gradient(90deg, var(--steel), var(--hv));
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 4px;
background: var(--steel-d);
border: 3px solid var(--hv);
cursor: grab;
}
input[type="range"]::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 4px;
background: var(--steel-d);
border: 3px solid var(--hv);
cursor: grab;
}
input[type="range"]:focus-visible {
outline: 3px solid var(--orange);
outline-offset: 4px;
}
.ramp {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.help {
font-size: 0.76rem;
color: var(--muted);
margin-top: 5px;
}
/* Quote rail */
.est-quote {
padding: 24px 26px;
background: var(--bg);
}
.quote-card {
background: var(--steel);
color: var(--white);
border-radius: var(--radius);
padding: 20px;
border-left: 6px solid var(--hv);
margin-bottom: 22px;
}
.quote-card__label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--hv);
}
.quote-card__range {
font-size: clamp(1.4rem, 5vw, 1.9rem);
font-weight: 800;
letter-spacing: -0.02em;
margin: 6px 0 14px;
}
.quote-meta {
list-style: none;
padding: 0;
border-top: 1px solid rgba(255, 255, 255, 0.14);
}
.quote-meta li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 7px 0;
font-size: 0.83rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.quote-meta span {
color: #b4bac2;
}
.quote-meta strong {
text-align: right;
font-weight: 600;
}
.quote-disclaimer {
font-size: 0.72rem;
color: #aeb4bc;
margin-top: 12px;
}
/* Contact */
.contact h2,
.confirm h2 {
font-size: 1.1rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: -0.01em;
margin-bottom: 14px;
color: var(--steel);
}
.error {
color: var(--orange);
font-size: 0.76rem;
font-weight: 600;
margin-top: 4px;
}
input.invalid {
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(232, 100, 42, 0.18);
}
.cta {
width: 100%;
padding: 14px;
background: var(--hv);
color: var(--ink);
border: 2px solid var(--steel-d);
border-radius: var(--radius);
font: inherit;
font-size: 0.95rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.03em;
cursor: pointer;
transition: transform 0.1s ease, background 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 4px 0 var(--steel-d);
}
.cta:hover {
background: #ffd633;
}
.cta:active {
transform: translateY(3px);
box-shadow: 0 1px 0 var(--steel-d);
}
.cta:focus-visible {
outline: 3px solid var(--orange);
outline-offset: 3px;
}
.trust {
text-align: center;
font-size: 0.76rem;
color: var(--muted);
margin-top: 10px;
}
/* Confirmation */
.confirm {
text-align: center;
padding: 20px 8px;
}
.confirm__check {
width: 56px;
height: 56px;
margin: 0 auto 14px;
border-radius: var(--radius);
background: var(--steel);
color: var(--hv);
font-size: 1.8rem;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid var(--hv);
}
.confirm p {
font-size: 0.9rem;
color: var(--muted);
margin-bottom: 10px;
}
.confirm strong {
color: var(--ink);
}
.confirm__range {
background: var(--bg);
border-radius: var(--radius);
padding: 10px;
margin: 14px 0;
font-size: 0.95rem !important;
}
.ghost {
background: transparent;
border: 1.5px solid var(--steel);
color: var(--steel);
padding: 11px 18px;
border-radius: var(--radius);
font: inherit;
font-weight: 700;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.ghost:hover {
background: var(--steel);
color: var(--white);
}
.ghost:focus-visible {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
/* Responsive */
@media (max-width: 520px) {
body {
padding: 0;
}
.estimate {
border-radius: 0;
border-left: none;
border-right: none;
}
.est-grid {
grid-template-columns: 1fr;
}
.est-config {
border-right: none;
border-bottom: 1px solid var(--line);
}
.checks {
grid-template-columns: 1fr;
}
.est-head__inner,
.est-config,
.est-quote {
padding-left: 18px;
padding-right: 18px;
}
}(function () {
"use strict";
// ---- Pricing model (illustrative only) ----------------------------------
// Base $/ft² and a fixed mobilization cost per project type.
var TYPES = {
kitchen: { label: "Kitchen remodel", rate: 165, base: 6500 },
bathroom: { label: "Bathroom", rate: 210, base: 4200 },
addition: { label: "Room addition", rate: 235, base: 9000 },
deck: { label: "Deck / patio", rate: 55, base: 2200 },
wholehome: { label: "Whole-home reno", rate: 145, base: 22000 }
};
var FINISH = {
1: { label: "Builder-grade", mult: 0.82 },
2: { label: "Mid-range", mult: 1.0 },
3: { label: "Premium", mult: 1.32 },
4: { label: "Luxury", mult: 1.75 }
};
// ---- DOM ----------------------------------------------------------------
var configForm = document.getElementById("config-form");
var sqftInput = document.getElementById("sqft");
var finishRng = document.getElementById("finish");
var finishLbl = document.getElementById("finish-label");
var timelineEl = document.getElementById("timeline");
var rangeOut = document.getElementById("range-out");
var metaType = document.getElementById("meta-type");
var metaSize = document.getElementById("meta-size");
var metaScope = document.getElementById("meta-scope");
var contactForm = document.getElementById("contact-form");
var confirmBox = document.getElementById("confirm");
var resetBtn = document.getElementById("reset-btn");
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function getType() {
var checked = configForm.querySelector('input[name="ptype"]:checked');
return TYPES[checked ? checked.value : "kitchen"];
}
function getScopeAddons() {
var boxes = configForm.querySelectorAll('input[name="scope"]:checked');
var total = 0;
boxes.forEach(function (b) {
total += Number(b.getAttribute("data-add")) || 0;
});
return { total: total, count: boxes.length };
}
function calc() {
var type = getType();
var sqft = Math.max(40, Math.min(6000, Number(sqftInput.value) || 0));
var finish = FINISH[finishRng.value] || FINISH[2];
var scope = getScopeAddons();
var timeMult = Number(timelineEl.value) || 1;
// Core build cost scaled by size and finish, plus selected scope add-ons.
var core = (type.base + type.rate * sqft) * finish.mult;
var mid = (core + scope.total) * timeMult;
// Quote a +/- band that tightens for smaller jobs.
var low = mid * 0.88;
var high = mid * 1.16;
rangeOut.textContent = money(low) + " – " + money(high);
metaType.textContent = type.label;
metaSize.textContent = sqft.toLocaleString("en-US") + " ft² · " + finish.label;
metaScope.textContent =
scope.count + " selected" +
(scope.count ? " · +" + money(scope.total) : "");
finishLbl.textContent = finish.label;
return { low: low, high: high };
}
// Recalculate on any configurator change
configForm.addEventListener("input", calc);
configForm.addEventListener("change", calc);
// ---- Validation ---------------------------------------------------------
var fields = {
name: { el: document.getElementById("name"), err: document.getElementById("err-name") },
email: { el: document.getElementById("email"), err: document.getElementById("err-email") },
phone: { el: document.getElementById("phone"), err: document.getElementById("err-phone") }
};
function validName(v) { return v.trim().length >= 2; }
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim()); }
function validPhone(v) {
var digits = v.replace(/\D/g, "");
return digits.length >= 7 && digits.length <= 15;
}
var validators = { name: validName, email: validEmail, phone: validPhone };
function check(key) {
var f = fields[key];
var ok = validators[key](f.el.value);
f.el.classList.toggle("invalid", !ok);
f.err.hidden = ok;
if (!ok) f.el.setAttribute("aria-invalid", "true");
else f.el.removeAttribute("aria-invalid");
return ok;
}
Object.keys(fields).forEach(function (key) {
fields[key].el.addEventListener("blur", function () { check(key); });
fields[key].el.addEventListener("input", function () {
if (fields[key].el.classList.contains("invalid")) check(key);
});
});
contactForm.addEventListener("submit", function (e) {
e.preventDefault();
var ok = true;
Object.keys(fields).forEach(function (key) {
if (!check(key)) ok = false;
});
if (!ok) {
var firstBad = contactForm.querySelector(".invalid");
if (firstBad) firstBad.focus();
return;
}
var range = calc();
document.getElementById("confirm-name").textContent =
fields.name.el.value.trim().split(/\s+/)[0] || "there";
document.getElementById("confirm-phone").textContent = fields.phone.el.value.trim();
document.getElementById("confirm-range").textContent =
money(range.low) + " – " + money(range.high);
contactForm.hidden = true;
confirmBox.hidden = false;
confirmBox.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
resetBtn.addEventListener("click", function () {
contactForm.reset();
Object.keys(fields).forEach(function (key) {
fields[key].el.classList.remove("invalid");
fields[key].el.removeAttribute("aria-invalid");
fields[key].err.hidden = true;
});
confirmBox.hidden = true;
contactForm.hidden = false;
fields.name.el.focus();
});
// Initial paint
calc();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Instant Estimate Request — Foreman Build Co.</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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="estimate" aria-labelledby="est-title">
<header class="est-head">
<div class="est-head__bar" aria-hidden="true"></div>
<div class="est-head__inner">
<p class="est-kicker">Foreman Build Co. · Licensed & Insured</p>
<h1 id="est-title">Instant Estimate Request</h1>
<p class="est-sub">Get a ballpark range in seconds — then request a detailed, walked-through quote.</p>
</div>
</header>
<div class="est-grid">
<!-- Configurator -->
<section class="est-config" aria-label="Project details">
<form id="config-form" novalidate>
<fieldset class="block">
<legend>Project type</legend>
<div class="seg" role="radiogroup" aria-label="Project type">
<label class="seg__opt">
<input type="radio" name="ptype" value="kitchen" checked />
<span>Kitchen remodel</span>
</label>
<label class="seg__opt">
<input type="radio" name="ptype" value="bathroom" />
<span>Bathroom</span>
</label>
<label class="seg__opt">
<input type="radio" name="ptype" value="addition" />
<span>Room addition</span>
</label>
<label class="seg__opt">
<input type="radio" name="ptype" value="deck" />
<span>Deck / patio</span>
</label>
<label class="seg__opt">
<input type="radio" name="ptype" value="wholehome" />
<span>Whole-home reno</span>
</label>
</div>
</fieldset>
<fieldset class="block">
<legend>Scope of work</legend>
<div class="checks">
<label class="check"><input type="checkbox" name="scope" value="demo" data-add="2200" /><span>Demolition & haul-away</span></label>
<label class="check"><input type="checkbox" name="scope" value="plumbing" data-add="4800" /><span>Plumbing</span></label>
<label class="check"><input type="checkbox" name="scope" value="electrical" data-add="3600" /><span>Electrical</span></label>
<label class="check"><input type="checkbox" name="scope" value="hvac" data-add="5200" /><span>HVAC</span></label>
<label class="check"><input type="checkbox" name="scope" value="cabinetry" data-add="6500" /><span>Custom cabinetry</span></label>
<label class="check"><input type="checkbox" name="scope" value="permits" data-add="1400" /><span>Permits & inspection</span></label>
</div>
</fieldset>
<fieldset class="block">
<legend>Size & finish</legend>
<div class="field">
<label for="sqft">Square footage</label>
<div class="sqft-row">
<input type="number" id="sqft" name="sqft" min="40" max="6000" step="10" value="220" inputmode="numeric" />
<span class="unit">ft²</span>
</div>
</div>
<div class="field">
<label for="finish">Finish level: <strong id="finish-label">Mid-range</strong></label>
<input type="range" id="finish" name="finish" min="1" max="4" step="1" value="2" aria-describedby="finish-help" />
<div class="ramp" aria-hidden="true">
<span>Builder</span><span>Mid</span><span>Premium</span><span>Luxury</span>
</div>
<p id="finish-help" class="help">Materials, fixtures and craftsmanship tier.</p>
</div>
<div class="field">
<label for="timeline">Timeline</label>
<select id="timeline" name="timeline">
<option value="1.18">ASAP — rush (within 4 weeks)</option>
<option value="1" selected>Standard (2–4 months)</option>
<option value="0.94">Flexible (6+ months)</option>
</select>
</div>
</fieldset>
</form>
</section>
<!-- Estimate + contact -->
<aside class="est-quote" aria-label="Estimate and contact">
<div class="quote-card" aria-live="polite">
<p class="quote-card__label">Estimated project cost</p>
<p class="quote-card__range" id="range-out">$18,400 – $24,900</p>
<ul class="quote-meta">
<li><span>Project</span><strong id="meta-type">Kitchen remodel</strong></li>
<li><span>Size</span><strong id="meta-size">220 ft² · Mid-range</strong></li>
<li><span>Scope items</span><strong id="meta-scope">0 selected</strong></li>
</ul>
<p class="quote-disclaimer">Ballpark only — final pricing set after an on-site walkthrough.</p>
</div>
<form id="contact-form" class="contact" novalidate>
<h2>Request a detailed quote</h2>
<div class="field">
<label for="name">Full name</label>
<input type="text" id="name" name="name" autocomplete="name" placeholder="e.g. Dana Whitfield" />
<p class="error" id="err-name" hidden>Please enter your name.</p>
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" placeholder="[email protected]" />
<p class="error" id="err-email" hidden>Enter a valid email address.</p>
</div>
<div class="field">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" autocomplete="tel" placeholder="(512) 555-0148" />
<p class="error" id="err-phone" hidden>Enter a valid phone number.</p>
</div>
<div class="field">
<label for="zip">Project ZIP</label>
<input type="text" id="zip" name="zip" inputmode="numeric" placeholder="78704" />
</div>
<button type="submit" class="cta" id="submit-btn">Request detailed quote</button>
<p class="trust">No obligation · We reply within 1 business day</p>
</form>
<div class="confirm" id="confirm" hidden role="status">
<div class="confirm__check" aria-hidden="true">✓</div>
<h2>Request received</h2>
<p>Thanks, <strong id="confirm-name">there</strong>. A project manager will call <strong id="confirm-phone">you</strong> within one business day to schedule your walkthrough.</p>
<p class="confirm__range">Your ballpark: <strong id="confirm-range">—</strong></p>
<button type="button" class="ghost" id="reset-btn">Start a new estimate</button>
</div>
</aside>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Instant Estimate Request
A sturdy, hi-vis estimate request panel for a general contractor. Visitors pick a project type, tick the scope items they need, enter square footage, drag a finish level slider from Builder-grade to Luxury, and choose a timeline — and a live ballpark price range updates on every change with a project-size readout.
The right rail collects contact details with real labels, inline validation and focus styles. The Request detailed quote button checks for a valid name, email and phone before flipping into a confirmation state that quotes the current range and a follow-up window. Everything is vanilla JS — no libraries, no network.
The look leans rugged: steel header with a diagonal hazard stripe, concrete background, chunky bold display headings and a single hi-vis yellow CTA used sparingly.
Illustrative UI only — estimates are not binding and not a real construction quote.