Open demo

CSS code

HTML
<main class="pl">
  <header class="pl-top">
    <a class="pl-logo" href="#"><svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="13" fill="#0f766e"/><path d="M7 11h9a2.5 2.5 0 1 0-2.5-2.5M7 15h12a2.5 2.5 0 1 1-2.5 2.5" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>stillair</a>
    <p class="pl-date">Launching March 2027</p>
  </header>
  <div class="pl-hero">
    <section aria-labelledby="pl-title">
      <p class="pl-eyebrow">Aero One air purifier</p>
      <h1 class="pl-title" id="pl-title">Clean air you will not hear.</h1>
      <p class="pl-lead">A purifier for bedrooms and small flats that clears a 30 m² room in twelve minutes and then all but disappears. Join the waitlist to hear first when orders open.</p>
      <form class="pl-form" data-signup="pl-done" action="#" method="post">
        <label class="sr-only" for="pl-email">Email address</label>
        <div class="pl-field">
          <input id="pl-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="pl-err">
          <button type="submit">Join the waitlist</button>
        </div>
        <p class="pl-err" id="pl-err" data-error aria-live="polite"></p>
        <p class="pl-proof"><span class="pl-faces" aria-hidden="true"><span></span><span></span><span></span></span>1,284 people are waiting. The first 500 get 20% off.</p>
      </form>
      <div class="pl-done" id="pl-done" tabindex="-1" hidden><p><strong>You are on the waitlist.</strong> We will email you when orders open, with your early price.</p></div>
    </section>
    <div class="pl-visual">
      <svg viewBox="0 0 320 380" role="img" aria-label="Aero One, a white cylindrical air purifier with a teal top ring">
        <defs><linearGradient id="pl-body" x1="0" x2="1"><stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#f4f7f6"/><stop offset="1" stop-color="#d9e2df"/></linearGradient></defs>
        <circle cx="160" cy="185" r="150" fill="#dcefe9"/>
        <circle cx="160" cy="185" r="112" fill="none" stroke="#b7ddd3" stroke-dasharray="3 7"/>
        <ellipse cx="160" cy="342" rx="78" ry="10" fill="#0c1f1a" opacity=".12"/>
        <rect x="98" y="58" width="124" height="282" rx="56" fill="url(#pl-body)" stroke="#c9d6d2"/>
        <ellipse cx="160" cy="76" rx="46" ry="9" fill="#0f766e"/>
        <ellipse cx="160" cy="74" rx="38" ry="5" fill="#14b8a6"/>
        <g stroke="#cbd8d4" stroke-width="3" stroke-linecap="round"><path d="M118 160v150M130 150v168M142 146v176M154 144v180M166 144v180M178 146v176M190 150v168M202 160v150"/></g>
        <circle cx="160" cy="118" r="5" fill="#14b8a6"/>
      </svg>
    </div>
  </div>
  <ul class="pl-teasers" aria-label="Feature teasers">
    <li class="pl-card"><span class="pl-num">01 / 03</span><span class="pl-ic" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></span><h2>28 dB on its night setting</h2><p>Quieter than a library reading room, so it can run all night beside the bed.</p></li>
    <li class="pl-card"><span class="pl-num">02 / 03</span><span class="pl-ic" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg></span><h2>One filter a year</h2><p>An H13 filter rated for twelve months of daily use, swapped in ten seconds without tools.</p></li>
    <li class="pl-card locked"><span class="pl-num">03 / 03</span><span class="pl-ic" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span><h2>Feature three is under wraps</h2><p>Revealed to the waitlist first, on 12 February.</p></li>
  </ul>
</main>
CSS
/* Product launch page: hero with a waitlist form and a product render, then three feature teasers (the last one still locked). */
.pl{--pl-bg:#f3f6f4;--pl-ink:#0c1f1a;--pl-muted:#4b5f59;--pl-accent:#0f766e;--pl-accent-hover:#115e59;--pl-line:#d5e0dc;--pl-field:#7f938d;--pl-err:#b42318;
  line-height:1.5;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;padding:26px clamp(20px,5vw,56px) 40px;
  color:var(--pl-ink);background:var(--pl-bg);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.pl [hidden]{display:none!important}
.pl a{color:inherit}
.pl a:focus-visible,.pl button:focus-visible,.pl input:focus-visible{outline:2px solid var(--pl-accent);outline-offset:3px}
.pl-top,.pl-hero,.pl-teasers{width:100%;max-width:1120px;margin:0 auto}
.pl-top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.pl-logo{display:inline-flex;align-items:center;gap:9px;font-weight:700;font-size:1.1rem;letter-spacing:-.02em;text-decoration:none}
.pl-date{margin:0;padding:6px 14px;font-size:.82rem;font-weight:600;color:var(--pl-accent-hover);background:#dcefe9;border-radius:999px}
.pl-hero{flex:1;display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:40px;align-items:center;padding:40px 0}
.pl-eyebrow{margin:0 0 14px;font-size:.8rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pl-accent)}
.pl-title{margin:0 0 18px;font-size:clamp(2.3rem,5.6vw,4rem);font-weight:700;line-height:1.02;letter-spacing:-.045em}
.pl-lead{max-width:500px;margin:0 0 28px;font-size:1.08rem;line-height:1.6;color:var(--pl-muted)}
.pl-form{max-width:470px}
.pl-field{display:flex;gap:10px}
.pl-field input{flex:1;min-width:0;height:52px;padding:0 18px;font:inherit;color:var(--pl-ink);background:#fff;border:1px solid var(--pl-field);border-radius:12px}
.pl-field input::placeholder{color:#6b7f79}
.pl-field input[aria-invalid="true"]{border-color:var(--pl-err);box-shadow:0 0 0 1px var(--pl-err)}
.pl-field button{flex:none;height:52px;padding:0 24px;font:inherit;font-weight:700;color:#fff;background:var(--pl-accent);border:0;border-radius:12px;cursor:pointer;
  box-shadow:0 10px 24px -12px rgba(15,118,110,.7);transition:background-color .2s}
.pl-field button:hover{background:var(--pl-accent-hover)}
.pl-err{min-height:1.4em;margin:8px 0 0;font-size:.88rem;color:var(--pl-err)}
.pl-proof{display:flex;align-items:center;gap:10px;margin:2px 0 0;font-size:.88rem;color:var(--pl-muted)}
.pl-faces{display:flex}
.pl-faces span{width:26px;height:26px;margin-left:-8px;border:2px solid var(--pl-bg);border-radius:50%}
.pl-faces span:first-child{margin-left:0;background:#f4a261}
.pl-faces span:nth-child(2){background:#2a9d8f}
.pl-faces span:nth-child(3){background:#8d99ae}
.pl-done{max-width:470px;padding:16px 20px;background:#dcefe9;color:#0b3f3a;border-radius:14px;line-height:1.5}
.pl-done:focus{outline:none}
.pl-done p{margin:0}
.pl-visual{display:grid;place-items:center}
.pl-visual svg{width:100%;max-width:340px;height:auto}
.pl-teasers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;list-style:none;padding:0}
.pl-card{position:relative;padding:22px;background:#fff;border:1px solid var(--pl-line);border-radius:18px}
.pl-num{position:absolute;top:20px;right:22px;font-size:.75rem;font-weight:700;color:var(--pl-muted);font-variant-numeric:tabular-nums}
.pl-ic{display:grid;place-items:center;width:40px;height:40px;margin:0 0 14px;color:var(--pl-accent);background:#e3f1ed;border-radius:12px}
.pl-card h2{margin:0 0 6px;font-size:1.02rem;letter-spacing:-.01em}
.pl-card p{margin:0;font-size:.9rem;line-height:1.55;color:var(--pl-muted)}
.pl-card.locked{background:repeating-linear-gradient(-45deg,#fff 0 10px,#f5f8f7 10px 20px);border-style:dashed;border-color:#9fb1ab}
.pl-card.locked .pl-ic{color:var(--pl-muted);background:#edf1ef}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:991.98px){.pl-hero{grid-template-columns:1fr;gap:12px}.pl-visual{order:-1}.pl-visual svg{max-width:220px}.pl-teasers{grid-template-columns:1fr}}
@media (max-width:575.98px){.pl-field{flex-direction:column}.pl-field input{flex:none}.pl-field button{width:100%}}
@media (prefers-reduced-motion:reduce){.pl *{transition:none}}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<main class="pl d-flex flex-column">
  <header class="pl-wide w-100 mx-auto d-flex flex-wrap align-items-center justify-content-between gap-3">
    <a class="pl-logo d-inline-flex align-items-center gap-2 fw-bold" href="#"><svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="13" fill="#0f766e"/><path d="M7 11h9a2.5 2.5 0 1 0-2.5-2.5M7 15h12a2.5 2.5 0 1 1-2.5 2.5" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>stillair</a>
    <p class="pl-date badge rounded-pill m-0">Launching March 2027</p>
  </header>
  <div class="pl-wide w-100 mx-auto flex-grow-1 d-flex flex-column justify-content-center py-5">
  <div class="row align-items-center gx-4 gy-3">
    <section class="col-lg-7" aria-labelledby="pl-title">
      <p class="pl-eyebrow fw-bold text-uppercase mb-3">Aero One air purifier</p>
      <h1 class="pl-title fw-bold mb-3" id="pl-title">Clean air you will not hear.</h1>
      <p class="pl-lead text-body-secondary mb-4">A purifier for bedrooms and small flats that clears a 30 m² room in twelve minutes and then all but disappears. Join the waitlist to hear first when orders open.</p>
      <form class="pl-form" data-signup="pl-done" action="#" method="post">
        <label class="visually-hidden" for="pl-email">Email address</label>
        <div class="d-flex flex-column flex-sm-row gap-2">
          <input class="form-control" id="pl-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="pl-err">
          <button class="btn btn-pl flex-shrink-0" type="submit">Join the waitlist</button>
        </div>
        <p class="pl-err small mt-2 mb-0" id="pl-err" data-error aria-live="polite"></p>
        <p class="d-flex align-items-center gap-2 small text-body-secondary m-0"><span class="pl-faces d-flex" aria-hidden="true"><span></span><span></span><span></span></span>1,284 people are waiting. The first 500 get 20% off.</p>
      </form>
      <div class="pl-done px-4 py-3" id="pl-done" tabindex="-1" hidden><p class="m-0"><strong>You are on the waitlist.</strong> We will email you when orders open, with your early price.</p></div>
    </section>
    <div class="pl-visual col-lg-5 order-first order-lg-last d-grid justify-content-center">
      <svg viewBox="0 0 320 380" role="img" aria-label="Aero One, a white cylindrical air purifier with a teal top ring">
        <defs><linearGradient id="pl-body" x1="0" x2="1"><stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#f4f7f6"/><stop offset="1" stop-color="#d9e2df"/></linearGradient></defs>
        <circle cx="160" cy="185" r="150" fill="#dcefe9"/>
        <circle cx="160" cy="185" r="112" fill="none" stroke="#b7ddd3" stroke-dasharray="3 7"/>
        <ellipse cx="160" cy="342" rx="78" ry="10" fill="#0c1f1a" opacity=".12"/>
        <rect x="98" y="58" width="124" height="282" rx="56" fill="url(#pl-body)" stroke="#c9d6d2"/>
        <ellipse cx="160" cy="76" rx="46" ry="9" fill="#0f766e"/>
        <ellipse cx="160" cy="74" rx="38" ry="5" fill="#14b8a6"/>
        <g stroke="#cbd8d4" stroke-width="3" stroke-linecap="round"><path d="M118 160v150M130 150v168M142 146v176M154 144v180M166 144v180M178 146v176M190 150v168M202 160v150"/></g>
        <circle cx="160" cy="118" r="5" fill="#14b8a6"/>
      </svg>
    </div>
  </div>
  </div>
  <div class="pl-wide w-100 mx-auto">
  <ul class="row row-cols-1 row-cols-lg-3 g-3 list-unstyled mb-0" aria-label="Feature teasers">
    <li class="col"><div class="card h-100"><div class="card-body position-relative"><span class="pl-num position-absolute fw-bold text-body-secondary">01 / 03</span><span class="pl-ic d-grid align-content-center justify-content-center mb-3" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></span><h2 class="card-title fw-bold mb-1">28 dB on its night setting</h2><p class="card-text text-body-secondary">Quieter than a library reading room, so it can run all night beside the bed.</p></div></div></li>
    <li class="col"><div class="card h-100"><div class="card-body position-relative"><span class="pl-num position-absolute fw-bold text-body-secondary">02 / 03</span><span class="pl-ic d-grid align-content-center justify-content-center mb-3" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg></span><h2 class="card-title fw-bold mb-1">One filter a year</h2><p class="card-text text-body-secondary">An H13 filter rated for twelve months of daily use, swapped in ten seconds without tools.</p></div></div></li>
    <li class="col"><div class="card locked h-100"><div class="card-body position-relative"><span class="pl-num position-absolute fw-bold text-body-secondary">03 / 03</span><span class="pl-ic d-grid align-content-center justify-content-center mb-3" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span><h2 class="card-title fw-bold mb-1">Feature three is under wraps</h2><p class="card-text text-body-secondary">Revealed to the waitlist first, on 12 February.</p></div></div></li>
  </ul>
  </div>
</main>
CSS
/* Bootstrap grid for the hero, .card for the teasers, .badge for the date, .form-control and .btn for the waitlist. */
.pl{--pl-accent:#0f766e;--pl-accent-hover:#115e59;--pl-field:#7f938d;--pl-err:#b42318;
  --bs-body-bg:#f3f6f4;--bs-body-color:#0c1f1a;--bs-secondary-color:#4b5f59;--bs-border-color:#d5e0dc;
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  min-height:100dvh;padding:26px clamp(20px,5vw,56px) 40px;color:var(--bs-body-color);background:var(--bs-body-bg);font-family:var(--bs-body-font-family)}
.pl a{color:inherit}
.pl a:focus-visible{outline:2px solid var(--pl-accent);outline-offset:3px;border-radius:2px}
.pl-wide{max-width:1120px}
.pl-logo{font-size:1.1rem;letter-spacing:-.02em;text-decoration:none}
.pl-date{--bs-badge-font-size:.82rem;--bs-badge-font-weight:600;--bs-badge-padding-x:14px;--bs-badge-padding-y:6px;color:var(--pl-accent-hover);background:#dcefe9}
.pl-eyebrow{font-size:.8rem;letter-spacing:.14em;color:var(--pl-accent)}
.pl-title{font-size:clamp(2.3rem,5.6vw,4rem);line-height:1.02;letter-spacing:-.045em}
.pl-lead{max-width:500px;font-size:1.08rem;line-height:1.6}
.pl-form,.pl-done{max-width:470px}
.pl .form-control{height:52px;padding:0 18px;border-color:var(--pl-field);border-radius:12px}
.pl .form-control::placeholder{color:#6b7f79}
.pl .form-control:focus{border-color:var(--pl-field);box-shadow:none;outline:2px solid var(--pl-accent);outline-offset:3px}
.pl .form-control[aria-invalid="true"]{border-color:var(--pl-err);box-shadow:0 0 0 1px var(--pl-err)}
.pl .btn-pl{--bs-btn-bg:var(--pl-accent);--bs-btn-border-color:var(--pl-accent);--bs-btn-color:#fff;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;--bs-btn-padding-x:24px;
  --bs-btn-hover-bg:var(--pl-accent-hover);--bs-btn-hover-border-color:var(--pl-accent-hover);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--pl-accent-hover);--bs-btn-active-border-color:var(--pl-accent-hover);--bs-btn-active-color:#fff;
  --bs-btn-focus-box-shadow:0 0 0 3px var(--bs-body-bg),0 0 0 5px var(--pl-accent);height:52px;box-shadow:0 10px 24px -12px rgba(15,118,110,.7)}
.pl-err{min-height:1.4em;color:var(--pl-err)}
.pl-faces span{width:26px;height:26px;margin-left:-8px;border:2px solid var(--bs-body-bg);border-radius:50%}
.pl-faces span:first-child{margin-left:0;background:#f4a261}
.pl-faces span:nth-child(2){background:#2a9d8f}
.pl-faces span:nth-child(3){background:#8d99ae}
.pl-done{background:#dcefe9;color:#0b3f3a;border-radius:14px}
.pl-done:focus{outline:none}
.pl-visual svg{width:100%;max-width:340px;height:auto}
.pl .card{--bs-card-border-radius:18px;--bs-card-spacer-x:22px;--bs-card-spacer-y:22px}
.pl-num{top:20px;right:22px;font-size:.75rem;font-variant-numeric:tabular-nums}
.pl-ic{width:40px;height:40px;color:var(--pl-accent);background:#e3f1ed;border-radius:12px}
.pl .card-title{font-size:1.02rem;letter-spacing:-.01em}
.pl .card-text{font-size:.9rem;line-height:1.55}
.pl .card.locked{background:repeating-linear-gradient(-45deg,#fff 0 10px,#f5f8f7 10px 20px);border-style:dashed;border-color:#9fb1ab}
.pl .card.locked .pl-ic{color:var(--bs-secondary-color);background:#edf1ef}
@media (max-width:991.98px){.pl-visual svg{max-width:220px}}
@media (prefers-reduced-motion:reduce){.pl .btn,.pl .form-control{transition:none}}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<main class="flex min-h-dvh flex-col bg-pl-bg px-[clamp(20px,5vw,56px)] pt-[26px] pb-10 font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] text-pl-ink">
  <header class="mx-auto flex w-full max-w-[1120px] flex-wrap items-center justify-between gap-3">
    <a class="inline-flex items-center gap-[9px] text-[1.1rem] font-bold tracking-[-.02em] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-pl-accent" href="#"><svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="13" fill="#0f766e"/><path d="M7 11h9a2.5 2.5 0 1 0-2.5-2.5M7 15h12a2.5 2.5 0 1 1-2.5 2.5" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>stillair</a>
    <p class="rounded-full bg-[#dcefe9] px-3.5 py-1.5 text-[.82rem] font-semibold text-pl-accent-hover">Launching March 2027</p>
  </header>
  <div class="mx-auto grid w-full max-w-[1120px] flex-1 grid-cols-1 items-center gap-3 py-10 min-[992px]:grid-cols-[minmax(0,7fr)_minmax(0,5fr)] min-[992px]:gap-10">
    <section aria-labelledby="pl-title">
      <p class="mb-3.5 text-[.8rem] font-bold tracking-[.14em] text-pl-accent uppercase">Aero One air purifier</p>
      <h1 class="mb-[18px] text-[clamp(2.3rem,5.6vw,4rem)] leading-[1.02] font-bold tracking-[-.045em]" id="pl-title">Clean air you will not hear.</h1>
      <p class="mb-7 max-w-[500px] text-[1.08rem] leading-[1.6] text-pl-muted">A purifier for bedrooms and small flats that clears a 30 m² room in twelve minutes and then all but disappears. Join the waitlist to hear first when orders open.</p>
      <form class="max-w-[470px]" data-signup="pl-done" action="#" method="post">
        <label class="sr-only" for="pl-email">Email address</label>
        <div class="flex flex-col gap-2.5 min-[576px]:flex-row">
          <input class="h-[52px] min-w-0 flex-none min-[576px]:flex-1 rounded-xl border border-pl-field bg-white px-[18px] text-pl-ink placeholder:text-[#6b7f79] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-pl-accent aria-invalid:border-pl-err aria-invalid:shadow-[0_0_0_1px_var(--color-pl-err)]" id="pl-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="pl-err">
          <button class="h-[52px] flex-none cursor-pointer rounded-xl bg-pl-accent px-6 font-bold text-white shadow-[0_10px_24px_-12px_rgba(15,118,110,.7)] transition-colors duration-200 hover:bg-pl-accent-hover focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-pl-accent motion-reduce:transition-none" type="submit">Join the waitlist</button>
        </div>
        <p class="mt-2 min-h-[1.4em] text-[.88rem] text-pl-err" id="pl-err" data-error aria-live="polite"></p>
        <p class="mt-0.5 flex items-center gap-2.5 text-[.88rem] text-pl-muted"><span class="flex -space-x-2" aria-hidden="true"><span class="size-[26px] rounded-full border-2 border-pl-bg bg-[#f4a261]"></span><span class="size-[26px] rounded-full border-2 border-pl-bg bg-[#2a9d8f]"></span><span class="size-[26px] rounded-full border-2 border-pl-bg bg-[#8d99ae]"></span></span>1,284 people are waiting. The first 500 get 20% off.</p>
      </form>
      <div class="max-w-[470px] rounded-[14px] bg-[#dcefe9] px-5 py-4 leading-normal text-[#0b3f3a] focus:outline-none" id="pl-done" tabindex="-1" hidden><p><strong>You are on the waitlist.</strong> We will email you when orders open, with your early price.</p></div>
    </section>
    <div class="-order-1 grid place-items-center min-[992px]:order-none">
      <svg class="h-auto w-full max-w-[220px] min-[992px]:max-w-[340px]" viewBox="0 0 320 380" role="img" aria-label="Aero One, a white cylindrical air purifier with a teal top ring">
        <defs><linearGradient id="pl-body" x1="0" x2="1"><stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#f4f7f6"/><stop offset="1" stop-color="#d9e2df"/></linearGradient></defs>
        <circle cx="160" cy="185" r="150" fill="#dcefe9"/>
        <circle cx="160" cy="185" r="112" fill="none" stroke="#b7ddd3" stroke-dasharray="3 7"/>
        <ellipse cx="160" cy="342" rx="78" ry="10" fill="#0c1f1a" opacity=".12"/>
        <rect x="98" y="58" width="124" height="282" rx="56" fill="url(#pl-body)" stroke="#c9d6d2"/>
        <ellipse cx="160" cy="76" rx="46" ry="9" fill="#0f766e"/>
        <ellipse cx="160" cy="74" rx="38" ry="5" fill="#14b8a6"/>
        <g stroke="#cbd8d4" stroke-width="3" stroke-linecap="round"><path d="M118 160v150M130 150v168M142 146v176M154 144v180M166 144v180M178 146v176M190 150v168M202 160v150"/></g>
        <circle cx="160" cy="118" r="5" fill="#14b8a6"/>
      </svg>
    </div>
  </div>
  <ul class="mx-auto grid w-full max-w-[1120px] grid-cols-1 gap-4 min-[992px]:grid-cols-3" aria-label="Feature teasers">
    <li class="relative rounded-[18px] border border-pl-line bg-white p-[22px]"><span class="absolute top-5 right-[22px] text-[.75rem] font-bold text-pl-muted tabular-nums">01 / 03</span><span class="mb-3.5 grid size-10 place-items-center rounded-xl bg-[#e3f1ed] text-pl-accent" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></span><h2 class="mb-1.5 text-[1.02rem] font-bold tracking-[-.01em]">28 dB on its night setting</h2><p class="text-[.9rem] leading-[1.55] text-pl-muted">Quieter than a library reading room, so it can run all night beside the bed.</p></li>
    <li class="relative rounded-[18px] border border-pl-line bg-white p-[22px]"><span class="absolute top-5 right-[22px] text-[.75rem] font-bold text-pl-muted tabular-nums">02 / 03</span><span class="mb-3.5 grid size-10 place-items-center rounded-xl bg-[#e3f1ed] text-pl-accent" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg></span><h2 class="mb-1.5 text-[1.02rem] font-bold tracking-[-.01em]">One filter a year</h2><p class="text-[.9rem] leading-[1.55] text-pl-muted">An H13 filter rated for twelve months of daily use, swapped in ten seconds without tools.</p></li>
    <li class="relative rounded-[18px] border border-dashed border-[#9fb1ab] bg-[repeating-linear-gradient(-45deg,#fff_0_10px,#f5f8f7_10px_20px)] p-[22px]"><span class="absolute top-5 right-[22px] text-[.75rem] font-bold text-pl-muted tabular-nums">03 / 03</span><span class="mb-3.5 grid size-10 place-items-center rounded-xl bg-[#edf1ef] text-pl-muted" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span><h2 class="mb-1.5 text-[1.02rem] font-bold tracking-[-.01em]">Feature three is under wraps</h2><p class="text-[.9rem] leading-[1.55] text-pl-muted">Revealed to the waitlist first, on 12 February.</p></li>
  </ul>
</main>
CSS
@theme {
  --color-pl-bg: #f3f6f4;
  --color-pl-ink: #0c1f1a;
  --color-pl-muted: #4b5f59;
  --color-pl-accent: #0f766e;
  --color-pl-accent-hover: #115e59;
  --color-pl-line: #d5e0dc;
  --color-pl-field: #7f938d;
  --color-pl-err: #b42318;
}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

About this set

A pre-order style launch page for a physical product, with a hero and a row of feature teasers. The demo is a quiet air purifier: an eyebrow with the product name, a short headline, a paragraph with one concrete number, a waitlist form, and a social proof line with three avatar dots and the waitlist count. The product itself is an inline SVG render on a mint halo, so the page works before you have photography. Below the hero, three teaser cards are numbered 01 to 03: two reveal a feature with an icon, a title and one sentence, and the third is locked, with a dashed striped border and a padlock, saying when it will be revealed. Use it to build a waitlist in the weeks before orders open, and to give people a reason to come back. The hero is a 7 to 5 grid that stacks below 992 pixels, with the product moved above the text on small screens. The waitlist form is front end only: it validates the address and swaps in a confirmation message, and a script comment says where to connect a mailing-list provider.

What’s included

  • Hero with waitlist form, social proof line and SVG product render
  • Three numbered teaser cards, the last one locked until a reveal date
  • Product image moves above the text on small screens
  • Waitlist form with inline validation and a confirmation
  • Bootstrap version built from the grid, .card and .badge

Accessibility

The product render is an SVG with role="img" and a description; icons and avatar dots are aria-hidden. Teasers are a list of cards with their own h2 headings. The email field has a hidden label, an aria-live error linked with aria-describedby and aria-invalid when wrong, and focus moves to the confirmation on success. Links, the field and the button show a 2px teal focus outline; transitions are removed under prefers-reduced-motion.

Customise it

Change --pl-accent, --pl-bg and --pl-line on .pl (plain CSS and Bootstrap) or the --color-pl-* theme colours in Tailwind. Replace the SVG with a product photo, and add or remove teaser cards; the row is a three-column grid (row-cols-lg-3 in Bootstrap).