Files
2026-07-12 10:17:17 -04:00

4.5 KiB

Contact Form Pattern (Web3Forms)

Static-site contact form that emails the business owner directly. No backend, no calendar, no payment.

Form Fields

Keep it SHORT. Every extra field costs conversions.

Field Required Type Notes
Name Yes text autocomplete="name"
Phone Yes tel THE most important field for phone-first businesses
Email No email Optional — don't require it, it adds friction
Service Needed Yes select Dropdown with all service types + "Other"
Project Details Yes textarea 3-line minimum, gives context before callback
Lead Source No select Google/Facebook/Neighbor/Other — helps measure marketing

Deliberately missing: address, calendar, file upload, CAPTCHA (add only if spam), budget field.

HTML Structure

<div class="form-card" id="quote-form-wrapper">
  <div class="form-error" id="form-error" role="alert">
    Something went wrong. Please try again or call us directly.
  </div>

  <form id="quote-form" novalidate>
    <!-- fields with .form-group > .form-label + .form-input/.form-select/.form-textarea + .field-error -->
    <button type="submit" class="btn submit-btn">
      <span class="btn-text">Get My Free Quote</span>
      <span class="spinner"></span>
    </button>
  </form>

  <!-- Success state (hidden by default) -->
  <div class="form-success" id="form-success">
    <div class="check-icon"></div>
    <h3>Thanks, <span id="success-name">friend</span>!</h3>
    <p>We'll reach out within 2 hours.</p>
    <div class="emergency-note">
      <strong>🚨 Emergency?</strong> Call now: <a href="tel:...">(XXX) XXX-XXXX</a>
    </div>
  </div>
</div>

CSS States

.form-input.error, .form-select.error, .form-textarea.error {
  border-color: var(--color-emergency);
  box-shadow: 0 0 0 3px rgba(220,38,38,0.1);
}
.field-error { display: none; }
.field-error.visible { display: block; color: var(--color-emergency); }
.form-error { display: none; background: var(--color-emergency-light); }
.form-error.visible { display: block; }
.form-success { display: none; }
.form-success.visible { display: block; }
.submit-btn.loading .spinner { display: inline-block; }
.submit-btn.loading .btn-text { display: none; }

JavaScript

var WEB3FORMS_KEY = 'YOUR_ACCESS_KEY'; // Replace with real key
var WEB3FORMS_URL = 'https://api.web3forms.com/submit';

// Validation
function validate() {
  var valid = true;
  if (!nameField.value.trim()) { showErr('name'); valid = false; }
  if (!phoneField.value.trim()) { showErr('phone'); valid = false; }
  if (!detailsField.value.trim()) { showErr('details'); valid = false; }
  if (!serviceField.value) { showErr('service'); valid = false; }
  return valid;
}

// Clear errors on input
inputs.forEach(function(el) {
  el.addEventListener('input', function() {
    el.classList.remove('error');
    getErrorEl(el.name).classList.remove('visible');
  });
});

// Submit
form.addEventListener('submit', function(e) {
  e.preventDefault();
  if (!validate()) return;

  submitBtn.disabled = true;
  submitBtn.classList.add('loading');

  var payload = new FormData(form);
  payload.append('access_key', WEB3FORMS_KEY);
  payload.append('subject', 'New Quote Request — Business Name');
  payload.append('from_name', 'Business Website');

  fetch(WEB3FORMS_URL, { method: 'POST', body: payload })
    .then(function(res) { return res.json(); })
    .then(function(data) {
      if (data.success) {
        document.getElementById('success-name').textContent =
          nameField.value.trim().split(' ')[0] || 'friend';
        form.style.display = 'none';
        successDiv.classList.add('visible');
      } else {
        throw new Error(data.message || 'Submission failed');
      }
    })
    .catch(function(err) {
      submitBtn.disabled = false;
      submitBtn.classList.remove('loading');
      errorBanner.classList.add('visible');
      console.error('Form error:', err);
    });
});

Setup

  1. Go to https://web3forms.com/ — create free account
  2. Copy access key
  3. Replace YOUR_ACCESS_KEY in the form script
  4. Configure destination email in Web3Forms dashboard

Testing

  • file:// CORS blocks fetch() — expected. Test from a real web server.
  • Click submit with empty fields → validation errors on all required fields
  • Fill all required fields, submit → loading spinner → success card (if key valid) or error banner (if invalid key)
  • Success card shows customer's first name + emergency phone fallback