web design
COMPONENT V / ACCESS FORM · VALIDATION + STATES

Invite Only

Membership is by invitation.

Request a spot in the order. We will reach out to you soon.

Single opt-in. We will never share your details.

GUIDE / ACCESS FORM

Overview

A gated invitation form. Inline accessible errors validate on blur, the submit is disabled while a request is in flight, a hidden honeypot field catches bots, and a success state replaces the form once the request lands in the ledger.

When to use

The single conversion point. The hero and nav request-access affordances scroll here and focus the email field. This demo simulates the submit; the source posts through an access function that is a mock in dev and a serverless function in production.

Spec

Tokens
--gold focus, --ivory submit, #d9805f error
Motion
border-color .4s; arrow nudge on hover
A11y
aria-invalid, aria-describedby, role alert/status
States
idle, submitting, error, success

Anti-patterns

No submit without disabled-while-loading. No browser-only validation; inline errors must be reachable by screen readers. Never remove the honeypot.

<style>
:root{
  --bg:#0b0a08;--surface:#14110b;--ivory:#efe8d6;--muted:#9c9078;--faint:#5a5446;
  --line:#2a2519;--line-faint:rgba(239,232,214,.08);--gold:#c9a24b;--gold-soft:#e7cd8c;
  --font-display:'Instrument Serif',ui-serif,Georgia,'Times New Roman',serif;
  --font-title:'Libre Caslon Condensed',ui-serif,Georgia,'Times New Roman',serif;
  --font-body:'Inter',system-ui,-apple-system,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --ease:cubic-bezier(.22,1,.36,1);--dur-fast:200ms;--dur-base:400ms;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--ivory);font-family:var(--font-body);min-height:100vh;padding:64px 48px}
.af{max-width:28rem;margin:0 auto;text-align:center}
.af .kick{font-family:var(--font-mono);font-size:.7rem;font-weight:500;text-transform:uppercase;letter-spacing:.42em;color:var(--gold)}
.af h2{margin-top:24px;font-family:var(--font-title);font-weight:400;font-size:clamp(2rem,5vw,3rem);line-height:1.04;color:var(--ivory)}
.af h2 .g{color:var(--gold-soft)}
.af .lede{margin:20px auto 0;max-width:34rem;color:var(--muted);font-size:1rem;font-weight:300;line-height:1.7}
.af form{margin-top:48px;text-align:left;display:flex;flex-direction:column;gap:20px}
.af .hp{position:absolute;left:-9999px;height:0;width:0;opacity:0}
.af label{display:block;margin-bottom:8px;font-family:var(--font-mono);font-size:.75rem;text-transform:uppercase;letter-spacing:.2em;color:var(--faint)}
.af input{width:100%;border-radius:.75rem;border:1px solid var(--line);background:rgba(20,17,11,.6);padding:12px 16px;color:var(--ivory);font-family:var(--font-body);font-size:.9375rem;transition:border-color var(--dur-base) var(--ease)}
.af input::placeholder{color:rgba(90,84,70,.8)}
.af input:focus{outline:none;border-color:rgba(201,162,75,.6)}
.af input[aria-invalid="true"]{border-color:#d9805f}
.af .err{margin-top:8px;font-size:.75rem;color:#d9805f}
.af .submit{display:inline-flex;align-items:center;justify-content:center;gap:10px;border-radius:9999px;border:0;background:var(--ivory);color:var(--bg);padding:14px 32px;font-family:var(--font-body);font-size:.875rem;font-weight:500;letter-spacing:.01em;cursor:pointer;box-shadow:0 0 80px -24px rgba(201,162,75,.5);transition:background-color var(--dur-base) var(--ease)}
.af .submit:hover{background:var(--gold-soft)}
.af .submit:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
.af .submit:disabled{cursor:not-allowed;opacity:.6}
.af .submit svg{transition:transform var(--dur-base) var(--ease)}
.af .submit:hover svg{transform:translateX(2px)}
.af .fine{margin-top:4px;text-align:center;font-size:.75rem;font-weight:300;color:var(--faint)}
.af .note{color:#d9805f;font-size:.875rem}
.af .ok{max-width:28rem;margin:0 auto;border-radius:1rem;border:1px solid rgba(201,162,75,.3);background:rgba(20,17,11,.6);padding:40px 32px;text-align:center}
.af .ok .badge{display:flex;align-items:center;justify-content:center;height:2.5rem;width:2.5rem;margin:0 auto 16px;border-radius:9999px;border:1px solid rgba(201,162,75,.5)}
.af .ok h3{font-family:var(--font-title);font-weight:400;font-size:1.5rem;color:var(--ivory)}
.af .ok p{margin-top:8px;font-size:.875rem;font-weight:300;color:var(--muted)}
</style>

<div class="af" id="af">
  <p class="kick">Invite Only</p>
  <h2>Membership is by <span class="g">invitation</span>.</h2>
  <p class="lede">Request a spot in the order. We will reach out to you soon.</p>

  <form id="form" novalidate>
    <input class="hp" type="text" name="company" tabindex="-1" autocomplete="off" aria-hidden="true">
    <div>
      <label for="hf-name">Full name</label>
      <input id="hf-name" name="name" type="text" autocomplete="name" placeholder="Ada Lovelace">
      <p class="err" id="hf-name-err" hidden></p>
    </div>
    <div>
      <label for="hf-email">Email</label>
      <input id="hf-email" name="email" type="email" inputmode="email" autocomplete="email" placeholder="you@firm.com">
      <p class="err" id="hf-email-err" hidden></p>
    </div>
    <p class="note" id="hf-fail" hidden>Your request could not be sent. Please try again.</p>
    <button class="submit" id="submit" type="submit">
      <span id="submit-label">Request an Invitation</span>
      <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    </button>
    <p class="fine">Single opt-in. We will never share your details.</p>
  </form>
</div>

<script>
var form = document.getElementById("form");
var nameI = document.getElementById("hf-name");
var emailI = document.getElementById("hf-email");
var submit = document.getElementById("submit");
var label = document.getElementById("submit-label");

function showErr(id, msg){
  var el = document.getElementById(id);
  if(msg){ el.textContent = msg; el.hidden = false; }
  else el.hidden = true;
}
function validEmail(v){ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }

function validateName(){
  var ok = nameI.value.trim().length > 0;
  nameI.setAttribute("aria-invalid", ok ? "false" : "true");
  showErr("hf-name-err", ok ? "" : "Your full name is required.");
  return ok;
}
function validateEmail(){
  var v = emailI.value.trim();
  var ok = v.length > 0 && validEmail(v);
  emailI.setAttribute("aria-invalid", ok ? "false" : "true");
  showErr("hf-email-err", !v ? "An email is required." : (!validEmail(v) ? "Enter a valid email address." : ""));
  return ok;
}
nameI.addEventListener("blur", validateName);
emailI.addEventListener("blur", validateEmail);

form.addEventListener("submit", function(e){
  e.preventDefault();
  // Honeypot: if filled, silently drop.
  if(form.elements["company"].value) return;
  document.getElementById("hf-fail").hidden = true;
  var ok = validateName() & validateEmail();
  if(!ok) return;

  submit.disabled = true;
  label.textContent = "Sending…";
  // Simulated submit; the source posts through submitAccess().
  setTimeout(function(){
    submit.disabled = false;
    label.textContent = "Request an Invitation";
    var af = document.getElementById("af");
    af.innerHTML =
      '<div class="ok" role="status">' +
        '<div class="badge"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 13l4 4L19 7" stroke="#e7cd8c" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>' +
        '<h3>Your request has entered the ledger.</h3>' +
        '<p>A steward will reach out when a place opens.</p>' +
      '</div>';
  }, 900);
});
</script>