Open demo

CSS code

HTML
<main class="mn">
  <header class="mn-top">
    <a class="mn-logo" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="4" y="2" width="18" height="22" rx="3"/><path d="M9 9h8M9 13h8M9 17h5"/></svg>Linden & Row</a>
    <a class="mn-mail" href="mailto:[email protected]">[email protected]</a>
  </header>
  <section class="mn-body" aria-labelledby="mn-title">
    <p class="mn-eyebrow">Stationery shop · Opening in November</p>
    <h1 class="mn-title" id="mn-title">Good paper takes time. <em>So does a good shop.</em></h1>
    <p class="mn-lead">Notebooks, fountain pens and letterpress cards from twelve small makers, in one room on Gloucester Road. Leave your email and we will write once, on opening day.</p>
    <form class="mn-form" data-signup="mn-done" action="#" method="post">
      <label class="sr-only" for="mn-email">Email address</label>
      <div class="mn-field">
        <input id="mn-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="mn-err mn-note">
        <button type="submit">Notify me</button>
      </div>
      <p class="mn-err" id="mn-err" data-error aria-live="polite"></p>
      <p class="mn-note" id="mn-note">One email on opening day. No newsletter, no sharing.</p>
    </form>
    <div class="mn-done" id="mn-done" tabindex="-1" hidden>
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m7.5 12.5 3 3 6-6.5"/></svg>
      <p><strong>You are on the list.</strong> We will write once, on the morning the doors open.</p>
    </div>
  </section>
  <footer class="mn-foot">
    <nav aria-label="Elsewhere"><a href="#">Instagram</a><a href="#">Journal</a><a href="#">Makers</a></nav>
    <p>© 2026 Linden & Row, Bristol</p>
  </footer>
</main>
CSS
/* Minimal coming soon page: one message, one email field, nothing else competing for attention. */
.mn{--mn-bg:#f7f5f0;--mn-ink:#1c1917;--mn-muted:#57534e;--mn-accent:#1f4d3a;--mn-accent-hover:#163a2b;--mn-line:#857f76;--mn-err:#b42318;
  line-height:1.5;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;padding:28px clamp(20px,5vw,56px);
  background:repeating-linear-gradient(transparent 0 31px,rgba(28,25,23,.05) 31px 32px),var(--mn-bg);color:var(--mn-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.mn [hidden]{display:none!important}
.mn a{color:inherit;text-decoration:none}
.mn a:focus-visible,.mn button:focus-visible,.mn input:focus-visible{outline:2px solid var(--mn-accent);outline-offset:3px}
.mn-top{display:flex;align-items:center;justify-content:space-between;gap:12px 24px;flex-wrap:wrap}
.mn-logo{display:inline-flex;align-items:center;gap:10px;font-weight:700;font-size:1.02rem;letter-spacing:-.01em}
.mn-mail{font-size:.9rem;color:var(--mn-muted);border-bottom:1px solid currentColor;padding-bottom:1px}
.mn-mail:hover{color:var(--mn-ink)}
.mn-body{flex:1;display:flex;flex-direction:column;justify-content:center;width:100%;max-width:640px;margin:0 auto;padding:64px 0;text-align:center}
.mn-eyebrow{margin:0 0 22px;font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;color:var(--mn-muted)}
.mn-title{margin:0 0 22px;font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;font-weight:400;
  font-size:clamp(2.3rem,6vw,3.7rem);line-height:1.06;letter-spacing:-.02em}
.mn-title em{color:var(--mn-accent)}
.mn-lead{max-width:500px;margin:0 auto 34px;font-size:1.06rem;line-height:1.65;color:var(--mn-muted)}
.mn-form{width:100%;max-width:460px;margin:0 auto}
.mn-field{display:flex;gap:10px}
.mn-field input{flex:1;min-width:0;height:52px;padding:0 22px;font:inherit;color:var(--mn-ink);background:#fff;
  border:1px solid var(--mn-line);border-radius:999px}
.mn-field input::placeholder{color:#78716c}
.mn-field input[aria-invalid="true"]{border-color:var(--mn-err);box-shadow:0 0 0 1px var(--mn-err)}
.mn-field button{flex:none;height:52px;padding:0 28px;font:inherit;font-weight:600;color:#fff;background:var(--mn-accent);
  border:0;border-radius:999px;cursor:pointer;transition:background-color .2s}
.mn-field button:hover{background:var(--mn-accent-hover)}
.mn-err{min-height:1.4em;margin:10px 0 0;font-size:.88rem;color:var(--mn-err)}
.mn-note{margin:4px 0 0;font-size:.85rem;color:var(--mn-muted)}
.mn-done{max-width:460px;margin:0 auto;padding:18px 22px;display:flex;gap:12px;align-items:flex-start;text-align:left;
  background:#e3ece6;color:#173a2c;border-radius:18px;line-height:1.5}
.mn-done:focus{outline:none}
.mn-done svg{flex:none;margin-top:2px}
.mn-done p{margin:0}
.mn-foot{display:flex;align-items:center;justify-content:space-between;gap:10px 24px;flex-wrap:wrap;font-size:.85rem;color:var(--mn-muted)}
.mn-foot nav{display:flex;gap:22px}
.mn-foot nav a{border-bottom:1px solid transparent}
.mn-foot nav a:hover{color:var(--mn-ink);border-color:currentColor}
.mn-foot p{margin:0}
.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:575.98px){.mn-field{flex-direction:column}.mn-field input{flex:none}.mn-field button{width:100%}}
@media (prefers-reduced-motion:reduce){.mn *{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="mn d-flex flex-column">
  <header class="d-flex flex-wrap align-items-center justify-content-between gap-3">
    <a class="d-inline-flex align-items-center gap-2 fw-bold" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="4" y="2" width="18" height="22" rx="3"/><path d="M9 9h8M9 13h8M9 17h5"/></svg>Linden & Row</a>
    <a class="mn-mail" href="mailto:[email protected]">[email protected]</a>
  </header>
  <section class="mn-body flex-grow-1 d-flex flex-column justify-content-center w-100 mx-auto py-5 text-center" aria-labelledby="mn-title">
    <p class="mn-eyebrow text-uppercase text-body-secondary mb-4">Stationery shop · Opening in November</p>
    <h1 class="mn-title mb-4" id="mn-title">Good paper takes time. <em>So does a good shop.</em></h1>
    <p class="mn-lead text-body-secondary mx-auto mb-4 pb-2">Notebooks, fountain pens and letterpress cards from twelve small makers, in one room on Gloucester Road. Leave your email and we will write once, on opening day.</p>
    <form class="mn-form w-100 mx-auto" data-signup="mn-done" action="#" method="post">
      <label class="visually-hidden" for="mn-email">Email address</label>
      <div class="d-flex flex-column flex-sm-row gap-2">
        <input class="form-control rounded-pill" id="mn-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="mn-err mn-note">
        <button class="btn btn-mn rounded-pill flex-shrink-0" type="submit">Notify me</button>
      </div>
      <p class="mn-err small mt-2 mb-0" id="mn-err" data-error aria-live="polite"></p>
      <p class="small text-body-secondary mt-1 mb-0" id="mn-note">One email on opening day. No newsletter, no sharing.</p>
    </form>
    <div class="mn-done gap-2 align-items-start text-start mx-auto px-4 py-3" id="mn-done" tabindex="-1" hidden>
      <svg class="flex-shrink-0 mt-1" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m7.5 12.5 3 3 6-6.5"/></svg>
      <p class="m-0"><strong>You are on the list.</strong> We will write once, on the morning the doors open.</p>
    </div>
  </section>
  <footer class="mn-foot d-flex flex-wrap align-items-center justify-content-between gap-3 small text-body-secondary">
    <nav class="d-flex gap-4" aria-label="Elsewhere"><a href="#">Instagram</a><a href="#">Journal</a><a href="#">Makers</a></nav>
    <p class="m-0">© 2026 Linden & Row, Bristol</p>
  </footer>
</main>
CSS
/* Bootstrap utilities for layout, .form-control and .btn for the form, restyled through Bootstrap variables. */
.mn{--mn-accent:#1f4d3a;--mn-accent-hover:#163a2b;--mn-line:#857f76;--mn-err:#b42318;
  --bs-body-bg:#f7f5f0;--bs-body-color:#1c1917;--bs-secondary-color:#57534e;--bs-border-color:var(--mn-line);
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  min-height:100dvh;padding:28px clamp(20px,5vw,56px);background:repeating-linear-gradient(transparent 0 31px,rgba(28,25,23,.05) 31px 32px),var(--bs-body-bg);color:var(--bs-body-color);font-family:var(--bs-body-font-family)}
.mn a{color:inherit;text-decoration:none}
.mn a:focus-visible{outline:2px solid var(--mn-accent);outline-offset:3px;border-radius:2px}
.mn-mail{font-size:.9rem;color:var(--bs-secondary-color)!important;border-bottom:1px solid currentColor;padding-bottom:1px}
.mn-eyebrow{font-size:.78rem;letter-spacing:.16em}
.mn-title{font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;font-weight:400;font-size:clamp(2.3rem,6vw,3.7rem);line-height:1.06;letter-spacing:-.02em}
.mn-title em{color:var(--mn-accent)}
.mn-lead{max-width:500px;font-size:1.06rem;line-height:1.65}
.mn .form-control{height:52px;padding:0 22px;border-color:var(--mn-line);--bs-focus-ring-color:rgba(31,77,58,.25)}
.mn .form-control:focus{border-color:var(--mn-line);box-shadow:none;outline:2px solid var(--mn-accent);outline-offset:3px}
.mn .form-control::placeholder{color:#78716c}
.mn .form-control[aria-invalid="true"]{border-color:var(--mn-err);box-shadow:0 0 0 1px var(--mn-err)}
.mn .btn-mn{--bs-btn-bg:var(--mn-accent);--bs-btn-border-color:var(--mn-accent);--bs-btn-color:#fff;
  --bs-btn-hover-bg:var(--mn-accent-hover);--bs-btn-hover-border-color:var(--mn-accent-hover);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--mn-accent-hover);--bs-btn-active-border-color:var(--mn-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(--mn-accent);
  --bs-btn-padding-x:28px;--bs-btn-font-weight:600;height:52px}
.mn-err{min-height:1.4em;color:var(--mn-err)}
.mn-body{max-width:640px}
.mn-form{max-width:460px}
.mn-done{display:flex;max-width:460px;background:#e3ece6;color:#173a2c;border-radius:18px;line-height:1.5}
.mn-done:focus{outline:none}
.mn-foot nav a:hover,.mn-mail:hover{color:var(--bs-body-color)!important}
@media (prefers-reduced-motion:reduce){.mn .btn,.mn .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-mn-bg bg-[repeating-linear-gradient(transparent_0_31px,rgba(28,25,23,.05)_31px_32px)] px-[clamp(20px,5vw,56px)] py-7 font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] text-mn-ink">
  <header class="flex flex-wrap items-center justify-between gap-x-6 gap-y-3">
    <a class="inline-flex items-center gap-2.5 text-[1.02rem] font-bold tracking-[-.01em] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="4" y="2" width="18" height="22" rx="3"/><path d="M9 9h8M9 13h8M9 17h5"/></svg>Linden & Row</a>
    <a class="border-b border-current pb-px text-[.9rem] text-mn-muted hover:text-mn-ink focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent" href="mailto:[email protected]">[email protected]</a>
  </header>
  <section class="mx-auto flex w-full max-w-[640px] flex-1 flex-col justify-center py-16 text-center" aria-labelledby="mn-title">
    <p class="mb-[22px] text-[.78rem] tracking-[.16em] text-mn-muted uppercase">Stationery shop · Opening in November</p>
    <h1 class="mb-[22px] font-mn-serif text-[clamp(2.3rem,6vw,3.7rem)] leading-[1.06] font-normal tracking-[-.02em]" id="mn-title">Good paper takes time. <em class="text-mn-accent">So does a good shop.</em></h1>
    <p class="mx-auto mb-[34px] max-w-[500px] text-[1.06rem] leading-[1.65] text-mn-muted">Notebooks, fountain pens and letterpress cards from twelve small makers, in one room on Gloucester Road. Leave your email and we will write once, on opening day.</p>
    <form class="mx-auto w-full max-w-[460px]" data-signup="mn-done" action="#" method="post">
      <label class="sr-only" for="mn-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-full border border-mn-line bg-white px-[22px] text-mn-ink placeholder:text-[#78716c] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent aria-invalid:border-mn-err aria-invalid:shadow-[0_0_0_1px_var(--color-mn-err)]" id="mn-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="mn-err mn-note">
        <button class="h-[52px] flex-none cursor-pointer rounded-full bg-mn-accent px-7 font-semibold text-white transition-colors duration-200 hover:bg-mn-accent-hover focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent motion-reduce:transition-none" type="submit">Notify me</button>
      </div>
      <p class="mt-2.5 min-h-[1.4em] text-[.88rem] text-mn-err" id="mn-err" data-error aria-live="polite"></p>
      <p class="mt-1 text-[.85rem] text-mn-muted" id="mn-note">One email on opening day. No newsletter, no sharing.</p>
    </form>
    <div class="mx-auto flex max-w-[460px] items-start gap-3 rounded-[18px] bg-[#e3ece6] px-[22px] py-[18px] text-left leading-normal text-[#173a2c] focus:outline-none" id="mn-done" tabindex="-1" hidden>
      <svg class="mt-0.5 flex-none" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m7.5 12.5 3 3 6-6.5"/></svg>
      <p><strong>You are on the list.</strong> We will write once, on the morning the doors open.</p>
    </div>
  </section>
  <footer class="flex flex-wrap items-center justify-between gap-x-6 gap-y-2.5 text-[.85rem] text-mn-muted">
    <nav class="flex gap-[22px]" aria-label="Elsewhere"><a class="border-b border-transparent hover:border-current hover:text-mn-ink focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent" href="#">Instagram</a><a class="border-b border-transparent hover:border-current hover:text-mn-ink focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent" href="#">Journal</a><a class="border-b border-transparent hover:border-current hover:text-mn-ink focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mn-accent" href="#">Makers</a></nav>
    <p>© 2026 Linden & Row, Bristol</p>
  </footer>
</main>
CSS
@theme {
  --color-mn-bg: #f7f5f0;
  --color-mn-ink: #1c1917;
  --color-mn-muted: #57534e;
  --color-mn-accent: #1f4d3a;
  --color-mn-accent-hover: #163a2b;
  --color-mn-line: #857f76;
  --color-mn-err: #b42318;
  --font-mn-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
}
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 single-screen coming soon page that says one thing and asks for one thing: a serif headline, two lines of context and an email field with a Notify me button. The demo is a stationery shop opening in November, with the contact address in the top bar, three text links in the footer and a faint ruled-paper background. Use it when the launch date is loose and the only goal is to collect addresses, for a shop, a studio or a personal site. The page is a flex column that fills the viewport, so the header sits at the top, the footer at the bottom and the message in the middle at any height. The form is front end only. Without JavaScript the browser’s own required and type=”email” checks still work. With it, a few lines of script replace the browser bubble with a written message under the field, mark the input with aria-invalid, and on a valid address hide the form and show a confirmation. Nothing is sent: the script says where to connect a mailing-list provider. The field and button sit side by side and stack below 576 pixels.

What’s included

  • Serif headline, one email field and a footer, nothing else competing
  • Viewport-height flex layout that keeps the message centred
  • Written error message linked to the field with aria-describedby
  • Confirmation replaces the form and receives focus
  • Front end only: validates and confirms, never pretends to send

Accessibility

The email field has a label (visually hidden, so the placeholder is not the only name) and aria-describedby points at the error and the note below it. The error region is aria-live="polite", the field gets aria-invalid when the address is wrong, and focus returns to it. On success, focus moves to the confirmation. Every link, the field and the button show a 2px green focus outline, and the button's colour transition is removed under prefers-reduced-motion.

Customise it

Change --mn-bg, --mn-ink, --mn-muted and --mn-accent on .mn in plain CSS. The Bootstrap version sets the same variables plus --bs-body-bg and --bs-btn-* on .btn-mn; in Tailwind edit the --color-mn-* theme colours and --font-mn-serif.