Brand Applications
Every Men's Wellness Centers lead form follows one pattern. Dark navy surface, crisp white input fields, a single glowing orange CTA with an arrow, mobile-first sizing, and descriptive error handling that respects the person submitting it. Anything else is off-brand and converts worse.
Live Example
This is a working form. Try submitting it empty or with invalid data to see how error states feel. Every landing page, GHL widget, ad funnel, and WordPress lead form must match this pattern.
Do / Don't
Left: the approved 2026 pattern. Right: the retired WordPress form on menswellnesscenters.com/book-a-consultation. It is being replaced. Do not copy it.
Field States
Each field has five states. Build all five every time; do not ship a form that only handles the default state.
| State | Visual | When |
|---|---|---|
| Default | White field, 2px transparent border, gray placeholder | Page load, before user interaction |
| Focus | 2px orange border + 4px orange glow ring (rgba 232,103,10,0.22) | User tabs or taps into the field |
| Valid | White field, green checkmark on the right | User blurs the field with a valid value |
| Error | 2px red border, red icon, descriptive message below | User blurs with an invalid value, or submit fails validation |
| Disabled | Gray field, no glow, cursor: not-allowed | Only on the submit button while submitting |
Error Handling
Never say "Invalid" or "Required". Tell the user exactly what is wrong and exactly how to fix it. Field-level errors appear under the field. Submit-level errors appear in a banner at the top.
| Trigger | Error copy (use verbatim) |
|---|---|
| Name empty | Please enter your name so we can address you correctly. |
| Name too short | Name must be at least 2 characters. |
| Name has invalid characters | Please use only letters, spaces, hyphens, and apostrophes. |
| Phone empty | Please enter your phone number. We will call you within one business hour. |
| Phone too short | Phone number is too short. Please include the area code. |
| Phone not US | Phone number must be a valid US number. |
| Email invalid | That email does not look right. Please check and try again. |
| Location missing | Please select the center closest to you. |
| Consent unchecked | Please agree to receive messages so we can confirm your appointment. |
| Submit failed (network) | We could not submit your request. Please check your connection and try again, or call us directly at (804) 346-4636. |
Mobile
Most lead traffic is paid-media mobile. Test every form at 390px width first.
| Spec | Value | Why |
|---|---|---|
| Input font-size | 16px minimum | iOS Safari zooms when smaller than 16px. Always 16px+. |
| Tap target height | 48px minimum | WCAG and platform guidelines. Use 56-60px for primary CTAs. |
| Submit width | 100% on mobile | Full-width buttons are easier to thumb-tap. |
| Vertical spacing | 18px between fields | Fat-finger insurance. |
| Phone keyboard | inputmode="tel" | Pulls up the number pad on iOS and Android. |
| Autocomplete | autocomplete="name", "tel", "email" | Lets browsers and password managers prefill. |
| Form padding | 24px x 18px on mobile (was 36 x 32) | More content room without losing breathing space. |
Accessibility
<label>, not just a placeholder. Placeholders disappear on focus.required attribute and the validator is in JS, not native browser tooltips.role="alert" and are linked via aria-describedby.aria-busy="true" while submitting.role="status" and aria-live="polite".prefers-reduced-motion and prefers-contrast: more.Copy / Paste Kit
Copy the HTML, CSS, and JS into any landing page, GHL HTML block, or WordPress Custom HTML widget. Class names are prefixed with .mwc- so they will not collide with host themes.
<!-- =========================================================
MWC Lead Form Pattern v2026.06
Drop into any landing page, GHL HTML block, or WordPress
Custom HTML widget. Pair with mwc-leadform.css and
mwc-leadform.js. Replace ACTION_URL with your endpoint.
========================================================= -->
<form class="mwc-form" id="mwc-leadform" novalidate aria-labelledby="mwc-form-headline">
<div class="mwc-form__body">
<h2 class="mwc-form__headline" id="mwc-form-headline">Start feeling like yourself again.</h2>
<p class="mwc-form__sub">No-cost 60-minute in-person appointment. Same-day labs. No insurance needed.</p>
<!-- Form-wide error banner -->
<div class="mwc-form__error-banner" role="alert" aria-live="polite" id="mwc-form-banner">
Please correct the highlighted fields and try again.
</div>
<!-- NAME -->
<div class="mwc-field" data-field="name">
<label class="mwc-field__label" for="mwc-name">Name</label>
<svg class="mwc-field__icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
<input class="mwc-input" type="text" id="mwc-name" name="name" autocomplete="name" required minlength="2" placeholder="Your full name" aria-describedby="err-name">
<svg class="mwc-field__status mwc-field__status--ok" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
<svg class="mwc-field__status mwc-field__status--err" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
<p class="mwc-field__error" id="err-name" role="alert"></p>
</div>
<!-- PHONE -->
<div class="mwc-field" data-field="phone">
<label class="mwc-field__label" for="mwc-phone">Phone</label>
<svg class="mwc-field__icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56a.977.977 0 00-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 3 3 3.24 3 3.99 3 13.28 10.73 21 20.01 21c.71 0 .99-.63.99-1.18v-3.45c0-.54-.45-.99-.99-.99z"/></svg>
<input class="mwc-input" type="tel" id="mwc-phone" name="phone" autocomplete="tel" inputmode="tel" required placeholder="(555) 555-5555" aria-describedby="err-phone">
<svg class="mwc-field__status mwc-field__status--ok" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
<svg class="mwc-field__status mwc-field__status--err" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
<p class="mwc-field__error" id="err-phone" role="alert"></p>
</div>
<!-- LOCATIONS -->
<div class="mwc-locations" role="radiogroup" aria-labelledby="loc-label" data-field="location">
<p class="mwc-locations__label" id="loc-label">Choose your nearest center.</p>
<label class="mwc-loc">
<span class="mwc-loc__left">
<svg class="mwc-loc__pin" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
Glen Allen
</span>
<span class="mwc-loc__check"></span>
<input type="radio" name="location" value="glen-allen" required>
</label>
<label class="mwc-loc">
<span class="mwc-loc__left">
<svg class="mwc-loc__pin" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
Richmond
</span>
<span class="mwc-loc__check"></span>
<input type="radio" name="location" value="richmond" required>
</label>
<label class="mwc-loc">
<span class="mwc-loc__left">
<svg class="mwc-loc__pin" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
Mechanicsville
</span>
<span class="mwc-loc__check"></span>
<input type="radio" name="location" value="mechanicsville" required>
</label>
<label class="mwc-loc">
<span class="mwc-loc__left">
<svg class="mwc-loc__pin" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
Newport News
</span>
<span class="mwc-loc__check"></span>
<input type="radio" name="location" value="newport-news" required>
</label>
<label class="mwc-loc">
<span class="mwc-loc__left">
<svg class="mwc-loc__pin" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
Virginia Beach
</span>
<span class="mwc-loc__check"></span>
<input type="radio" name="location" value="virginia-beach" required>
</label>
<p class="mwc-field__error" id="err-location" role="alert"></p>
</div>
<!-- CONSENT -->
<label class="mwc-consent">
<input type="checkbox" name="consent" required>
<span>I agree to receive SMS/calls and texts from Men's Wellness Centers. Msg & data rates may apply. Reply STOP to opt out. Not a condition of service. HIPAA compliant. <a href="https://menswellnesscenters.com/privacy-policy/" target="_blank" rel="noopener">Privacy Policy</a></span>
</label>
<!-- SUBMIT -->
<button type="submit" class="mwc-submit">
<span class="mwc-submit__text">Book My Appointment</span>
<svg class="mwc-submit__arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
</div>
<!-- SUCCESS -->
<div class="mwc-form__success" role="status" aria-live="polite">
<div class="mwc-form__success-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
</div>
<h3 class="mwc-form__success-title">Got it. We will call you shortly.</h3>
<p class="mwc-form__success-body">A Men's Wellness Centers team member will call you within one business hour to schedule your 60-minute in-person appointment.</p>
</div>
</form>
/* =========================================================
MWC Lead Form Pattern v2026.06
Copy this whole file into any landing page, GHL form HTML
block, or WordPress page. Class names are namespaced with
.mwc- so they will not collide with host theme styles.
========================================================= */
/* ---- Container ---------------------------------------- */
.mwc-form {
background: #0B1029;
color: #F5F3F0;
border-radius: 16px;
padding: 36px 32px;
box-shadow: 0 24px 60px rgba(0,0,0,0.45);
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
max-width: 520px;
margin: 0 auto;
box-sizing: border-box;
}
.mwc-form *,
.mwc-form *::before,
.mwc-form *::after { box-sizing: border-box; }
/* ---- Headline + subhead ------------------------------ */
.mwc-form__headline {
font-family: 'Oswald', Impact, sans-serif;
font-weight: 700;
font-size: clamp(22px, 5vw, 28px);
text-transform: uppercase;
letter-spacing: 0.02em;
line-height: 1.15;
color: #FFFFFF;
margin: 0 0 10px;
}
.mwc-form__sub {
font-size: 14px;
line-height: 1.5;
color: #B0ADA8;
margin: 0 0 24px;
}
/* ---- Field group ------------------------------------- */
.mwc-field {
margin-bottom: 18px;
position: relative;
}
.mwc-field__label {
display: block;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #4F555C;
margin-bottom: 8px;
}
/* ---- Input + select ---------------------------------- */
.mwc-input,
.mwc-select {
width: 100%;
background: #FFFFFF;
color: #0B1029;
border: 2px solid transparent;
border-radius: 10px;
font-family: inherit;
font-size: 16px; /* iOS will not zoom on focus when >= 16 */
font-weight: 500;
padding: 14px 16px 14px 46px;
line-height: 1.3;
-webkit-appearance: none;
appearance: none;
transition: border-color 140ms ease, box-shadow 140ms ease;
}
.mwc-input:focus,
.mwc-select:focus {
outline: none;
border-color: #E8670A;
box-shadow: 0 0 0 4px rgba(232,103,10,0.22);
}
.mwc-input::placeholder { color: #4F555C; }
/* ---- Field icon (left) ------------------------------- */
.mwc-field__icon {
position: absolute;
left: 16px;
bottom: 14px;
width: 22px;
height: 22px;
color: #4F555C;
pointer-events: none;
}
/* ---- Validation status (right) ----------------------- */
.mwc-field__status {
position: absolute;
right: 16px;
bottom: 14px;
width: 22px;
height: 22px;
display: none;
pointer-events: none;
}
.mwc-field.is-valid .mwc-field__status--ok { display: block; color: #10B981; }
.mwc-field.is-error .mwc-field__status--err { display: block; color: #EF4444; }
/* ---- Error message ----------------------------------- */
.mwc-field__error {
display: none;
margin-top: 8px;
font-size: 13px;
line-height: 1.4;
color: #FCA5A5;
padding-left: 26px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23EF4444'><path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/></svg>") left center no-repeat;
background-size: 18px 18px;
}
.mwc-field.is-error .mwc-input,
.mwc-field.is-error .mwc-select { border-color: #EF4444; }
.mwc-field.is-error .mwc-field__error { display: block; }
/* ---- Center selector (radio chips) ------------------- */
.mwc-locations {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 18px;
}
.mwc-locations__label {
font-size: 13px;
color: #B0ADA8;
margin-bottom: 4px;
}
.mwc-loc {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border: 2px solid #1E244A;
background: #161B3A;
border-radius: 10px;
cursor: pointer;
transition: border-color 140ms ease, background 140ms ease;
}
.mwc-loc input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.mwc-loc__left {
display: flex;
align-items: center;
gap: 12px;
color: #FFFFFF;
font-weight: 600;
font-size: 15px;
}
.mwc-loc__pin {
width: 20px;
height: 20px;
color: #FFFFFF;
}
.mwc-loc__check {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid #4A5070;
position: relative;
flex-shrink: 0;
}
.mwc-loc:has(input:checked),
.mwc-loc.is-selected {
border-color: #E8670A;
background: rgba(232,103,10,0.10);
}
.mwc-loc:has(input:checked) .mwc-loc__pin,
.mwc-loc.is-selected .mwc-loc__pin { color: #E8670A; }
.mwc-loc:has(input:checked) .mwc-loc__check,
.mwc-loc.is-selected .mwc-loc__check {
background: #E8670A;
border-color: #E8670A;
}
.mwc-loc:has(input:checked) .mwc-loc__check::after,
.mwc-loc.is-selected .mwc-loc__check::after {
content: "";
position: absolute;
inset: 4px;
background: #FFFFFF;
border-radius: 50%;
}
/* ---- Consent block ----------------------------------- */
.mwc-consent {
display: flex;
gap: 12px;
align-items: flex-start;
margin: 6px 0 24px;
font-size: 13px;
line-height: 1.5;
color: #B0ADA8;
}
.mwc-consent input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 22px;
height: 22px;
flex-shrink: 0;
margin-top: 1px;
border: 2px solid #4A5070;
border-radius: 4px;
background: #161B3A;
cursor: pointer;
position: relative;
transition: background 140ms ease, border-color 140ms ease;
}
.mwc-consent input[type="checkbox"]:checked {
background: #E8670A;
border-color: #E8670A;
}
.mwc-consent input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 5px;
top: 1px;
width: 6px;
height: 12px;
border: solid #FFFFFF;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.mwc-consent a { color: #E8670A; text-decoration: none; }
.mwc-consent a:hover { text-decoration: underline; }
/* ---- Primary submit button (glow + arrow) ------------ */
.mwc-submit {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
background: #E8670A;
color: #FFFFFF;
font-family: inherit;
font-size: 16px;
font-weight: 700;
letter-spacing: 0.02em;
border: none;
border-radius: 10px;
padding: 18px 24px;
cursor: pointer;
box-shadow: 0 0 0 0 rgba(232,103,10,0.0),
0 12px 32px rgba(232,103,10,0.35),
0 0 28px rgba(232,103,10,0.45);
transition: box-shadow 200ms ease, transform 100ms ease, background 140ms ease;
}
.mwc-submit:hover {
background: #E8670A;
box-shadow: 0 16px 40px rgba(232,103,10,0.50),
0 0 44px rgba(232,103,10,0.65);
}
.mwc-submit:active { transform: translateY(1px); }
.mwc-submit:focus-visible {
outline: none;
box-shadow: 0 0 0 4px #FFFFFF,
0 0 0 8px #E8670A;
}
.mwc-submit[disabled] {
background: #4A5070;
color: #4F555C;
cursor: not-allowed;
box-shadow: none;
}
.mwc-submit__arrow {
width: 18px;
height: 18px;
transition: transform 200ms ease;
}
.mwc-submit:hover .mwc-submit__arrow { transform: translateX(4px); }
/* ---- Form-wide error banner -------------------------- */
.mwc-form__error-banner {
display: none;
margin: 0 0 16px;
padding: 12px 14px 12px 42px;
background: rgba(239,68,68,0.12);
border: 1px solid rgba(239,68,68,0.35);
border-radius: 8px;
color: #FCA5A5;
font-size: 13px;
line-height: 1.45;
position: relative;
}
.mwc-form.has-error .mwc-form__error-banner { display: block; }
.mwc-form__error-banner::before {
content: "";
position: absolute;
left: 12px;
top: 12px;
width: 22px;
height: 22px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23EF4444'><path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/></svg>") center/22px no-repeat;
}
/* ---- Success state ----------------------------------- */
.mwc-form__success {
display: none;
padding: 12px 0;
text-align: center;
}
.mwc-form.is-success .mwc-form__success { display: block; }
.mwc-form.is-success .mwc-form__body { display: none; }
.mwc-form__success-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: 50%;
background: rgba(16,185,129,0.12);
display: flex;
align-items: center;
justify-content: center;
color: #10B981;
}
.mwc-form__success-title {
font-family: 'Oswald', sans-serif;
font-size: 22px;
font-weight: 700;
color: #FFFFFF;
text-transform: uppercase;
letter-spacing: 0.02em;
margin: 0 0 8px;
}
.mwc-form__success-body {
font-size: 14px;
color: #B0ADA8;
line-height: 1.5;
}
/* ---- Loading state ----------------------------------- */
.mwc-form.is-submitting .mwc-submit { color: transparent; pointer-events: none; }
.mwc-form.is-submitting .mwc-submit::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.4);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: mwc-spin 700ms linear infinite;
}
@keyframes mwc-spin { to { transform: rotate(360deg); } }
.mwc-submit { position: relative; }
/* ---- Mobile breakpoints ------------------------------ */
@media (max-width: 480px) {
.mwc-form { padding: 24px 18px; border-radius: 12px; }
.mwc-form__headline { font-size: 22px; }
.mwc-input, .mwc-select { padding: 12px 14px 12px 42px; font-size: 16px; }
.mwc-field__icon { left: 14px; bottom: 12px; }
.mwc-field__status { right: 14px; bottom: 12px; }
.mwc-submit { padding: 16px 20px; font-size: 15px; }
}
/* ---- Reduced motion / contrast ----------------------- */
@media (prefers-reduced-motion: reduce) {
.mwc-input, .mwc-select, .mwc-submit, .mwc-submit__arrow, .mwc-loc, .mwc-consent input[type="checkbox"] {
transition: none;
}
.mwc-form.is-submitting .mwc-submit::after { animation: none; }
}
@media (prefers-contrast: more) {
.mwc-input, .mwc-select { border-color: #FFFFFF; }
}
/* =========================================================
MWC Lead Form Pattern v2026.06
Client-side validation + descriptive error handling +
submit lifecycle (loading -> success | error).
Hook real submission inside submitForm() below.
========================================================= */
(function () {
'use strict';
const form = document.getElementById('mwc-leadform');
if (!form) return;
const banner = form.querySelector('#mwc-form-banner');
const submitBtn = form.querySelector('.mwc-submit');
// ---- Validators ---------------------------------------------------------
const VALIDATORS = {
name: (value) => {
const v = value.trim();
if (!v) return 'Please enter your name so we can address you correctly.';
if (v.length < 2) return 'Name must be at least 2 characters.';
if (v.length > 80) return 'Name is too long. Please shorten to 80 characters.';
if (!/^[\p{L} '\-\.]+$/u.test(v)) return 'Please use only letters, spaces, hyphens, and apostrophes.';
return null;
},
phone: (value) => {
const digits = value.replace(/\D/g, '');
if (!digits) return 'Please enter your phone number. We will call you within one business hour.';
if (digits.length < 10) return 'Phone number is too short. Please include the area code.';
if (digits.length > 11) return 'Phone number is too long. US numbers are 10 digits.';
if (digits.length === 11 && digits[0] !== '1') return 'Phone number must be a valid US number.';
// Basic invalid-number guards
if (/^(\d)\1{9,}$/.test(digits.replace(/^1/, ''))) return 'Please enter a real phone number.';
return null;
},
email: (value) => {
const v = value.trim();
if (!v) return 'Please enter a valid email address.';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return 'That email does not look right. Please check and try again.';
if (v.length > 254) return 'Email is too long.';
return null;
},
location: () => {
const checked = form.querySelector('input[name="location"]:checked');
if (!checked) return 'Please select the center closest to you.';
return null;
},
consent: () => {
const c = form.querySelector('input[name="consent"]');
if (c && !c.checked) return 'Please agree to receive messages so we can confirm your appointment.';
return null;
}
};
// ---- Phone auto-format --------------------------------------------------
const phoneInput = form.querySelector('input[name="phone"]');
if (phoneInput) {
phoneInput.addEventListener('input', (e) => {
let d = e.target.value.replace(/\D/g, '').slice(0, 10);
if (d.length > 6) e.target.value = `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}`;
else if (d.length > 3) e.target.value = `(${d.slice(0,3)}) ${d.slice(3)}`;
else if (d.length > 0) e.target.value = `(${d}`;
else e.target.value = '';
});
}
// ---- Field state helpers ------------------------------------------------
function setError(field, message) {
const wrap = field.closest('.mwc-field, .mwc-locations');
if (!wrap) return;
wrap.classList.remove('is-valid');
wrap.classList.add('is-error');
const err = wrap.querySelector('.mwc-field__error');
if (err) err.textContent = message;
}
function setValid(field) {
const wrap = field.closest('.mwc-field, .mwc-locations');
if (!wrap) return;
wrap.classList.remove('is-error');
wrap.classList.add('is-valid');
const err = wrap.querySelector('.mwc-field__error');
if (err) err.textContent = '';
}
function clearState(field) {
const wrap = field.closest('.mwc-field, .mwc-locations');
if (!wrap) return;
wrap.classList.remove('is-error', 'is-valid');
const err = wrap.querySelector('.mwc-field__error');
if (err) err.textContent = '';
}
// ---- Field-level validation on blur + on input clear -------------------
form.querySelectorAll('input[name], select[name]').forEach((input) => {
const fn = VALIDATORS[input.name];
if (!fn) return;
input.addEventListener('blur', () => {
const msg = fn(input.value || '');
if (msg) setError(input, msg);
else setValid(input);
});
input.addEventListener('input', () => {
// Clear error as soon as the user types so they aren't yelled at
const wrap = input.closest('.mwc-field, .mwc-locations');
if (wrap && wrap.classList.contains('is-error')) clearState(input);
});
input.addEventListener('change', () => {
if (input.type === 'radio' || input.type === 'checkbox') {
const msg = fn(input.value || '');
if (msg) setError(input, msg);
else setValid(input);
}
});
});
// ---- Full validation ----------------------------------------------------
function validateAll() {
let firstErrorField = null;
Object.keys(VALIDATORS).forEach((key) => {
const field = form.querySelector(`[name="${key}"]`);
if (!field) return;
const msg = VALIDATORS[key](field.value || '');
if (msg) {
setError(field, msg);
if (!firstErrorField) firstErrorField = field;
} else {
setValid(field);
}
});
return firstErrorField;
}
// ---- Submission ---------------------------------------------------------
async function submitForm(payload) {
// REPLACE this stub with your real POST to GHL, Zapier, or your endpoint.
// Example for GHL inbound webhook:
//
// const res = await fetch('https://services.leadconnectorhq.com/hooks/.../webhook-trigger/...', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(payload)
// });
// if (!res.ok) throw new Error('Network response was not ok');
// return res.json();
//
// For demo: simulate 800ms latency + success
return new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 800));
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
form.classList.remove('has-error');
const firstError = validateAll();
if (firstError) {
form.classList.add('has-error');
banner.textContent = 'Please correct the highlighted fields and try again.';
firstError.focus({ preventScroll: false });
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
// Collect data
const data = Object.fromEntries(new FormData(form).entries());
data.source_url = window.location.href;
data.submitted_at = new Date().toISOString();
// Submit
form.classList.add('is-submitting');
submitBtn.setAttribute('aria-busy', 'true');
try {
await submitForm(data);
form.classList.remove('is-submitting');
form.classList.add('is-success');
// Optional: send GA / Meta / Google Ads conversion event here
// window.dataLayer && window.dataLayer.push({ event: 'lead_submit' });
} catch (err) {
form.classList.remove('is-submitting');
form.classList.add('has-error');
banner.textContent = 'We could not submit your request. Please check your connection and try again, or call us directly at (804) 346-4636.';
submitBtn.removeAttribute('aria-busy');
console.error('[mwc-leadform] submit failed:', err);
}
});
})();
Hard Rules