Open demo

CSS code

HTML
<main class="ng">
  <header class="ng-top">
    <a class="ng-logo" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="13" cy="13" r="3" fill="#ff4fd8" stroke="none"/><path d="M8 8a7 7 0 0 0 0 10M18 8a7 7 0 0 1 0 10" stroke="#ff4fd8"/><path d="M4.5 4.5a12 12 0 0 0 0 17M21.5 4.5a12 12 0 0 1 0 17" stroke="#3ef0ff"/></svg>Afterglow FM</a>
    <p>Online radio · Streaming from 1 November</p>
  </header>
  <section class="ng-body" aria-labelledby="ng-title">
    <p class="ng-onair">On air soon</p>
    <h1 class="ng-title" id="ng-title">Afterglow</h1>
    <p class="ng-lead">Synthwave, city pop and night-drive records, 24 hours a day, picked by people instead of an algorithm. Tune in when the signal goes live.</p>
    <button class="ng-open" type="button" popovertarget="ng-pop">Get the launch signal</button>
    <ul class="ng-facts">
      <li>24/7 stream</li>
      <li>No ads, ever</li>
      <li>Live DJ sets on Fridays</li>
    </ul>
  </section>
  <div class="ng-scene" aria-hidden="true"><span class="ng-sun"></span><span class="ng-floor"></span></div>
  <div class="ng-pop" id="ng-pop" popover role="dialog" aria-labelledby="ng-pop-title">
    <button class="ng-close" type="button" popovertarget="ng-pop" popovertargetaction="hide" aria-label="Close"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
    <h2 id="ng-pop-title">Be there when we go on air</h2>
    <p>One email when the stream starts, with the link and the first week's schedule.</p>
    <form data-signup="ng-done" action="#" method="post">
      <label for="ng-email">Email address</label>
      <div class="ng-field">
        <input id="ng-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ng-err" autofocus>
        <button type="submit">Send me the signal</button>
      </div>
      <p class="ng-err" id="ng-err" data-error aria-live="polite"></p>
    </form>
    <div class="ng-done" id="ng-done" tabindex="-1" hidden><p><strong>You are on the frequency.</strong> Look out for one email on 1 November.</p></div>
  </div>
</main>
CSS
/* Dark neon coming soon page. The sign-up form lives in a native popover: the button opens it with popovertarget,
   Esc and a click outside close it, and @starting-style animates it in. No JavaScript is needed to open or close it. */
.ng{--ng-bg:#07050f;--ng-ink:#f5f3ff;--ng-muted:#b8b0d6;--ng-pink:#ff4fd8;--ng-cyan:#3ef0ff;--ng-panel:#120c22;--ng-err:#ff9bb0;
  position:relative;isolation:isolate;line-height:1.5;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;padding:26px clamp(20px,5vw,56px);overflow:hidden;
  color:var(--ng-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:radial-gradient(70% 50% at 50% 0%,#2a0f45 0,transparent 70%),var(--ng-bg)}
.ng [hidden]{display:none!important}
.ng a{color:inherit}
.ng a:focus-visible,.ng button:focus-visible,.ng input:focus-visible{outline:2px solid var(--ng-cyan);outline-offset:3px}
.ng-top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:.85rem;color:var(--ng-muted)}
.ng-top p{margin:0}
.ng-logo{display:inline-flex;align-items:center;gap:9px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--ng-ink);text-decoration:none}
.ng-body{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px 0 calc(24vh + 150px);text-align:center}
.ng-onair{display:inline-flex;align-items:center;gap:8px;margin:0 0 18px;padding:5px 12px;font-size:.74rem;font-weight:800;letter-spacing:.2em;text-transform:uppercase;
  color:var(--ng-cyan);border:1px solid rgba(62,240,255,.55);border-radius:999px;box-shadow:0 0 18px -4px rgba(62,240,255,.6)}
.ng-onair::before{content:"";width:7px;height:7px;border-radius:50%;background:#ff5a7a;box-shadow:0 0 8px #ff5a7a;animation:ng-blink 1.6s ease-in-out infinite}
@keyframes ng-blink{50%{opacity:.25}}
.ng-title{margin:0 0 14px;font-size:clamp(3rem,11vw,7.5rem);font-weight:900;line-height:.95;letter-spacing:.04em;text-transform:uppercase;color:#fff0fb;
  text-shadow:0 0 6px var(--ng-pink),0 0 22px var(--ng-pink),0 0 48px #b01c9c;animation:ng-flicker 7s infinite}
@keyframes ng-flicker{0%,91%,93%,95%,100%{opacity:1}92%,94%{opacity:.55}}
.ng-lead{max-width:520px;margin:0 0 30px;font-size:1.08rem;line-height:1.6;color:var(--ng-muted)}
.ng-open{height:52px;padding:0 28px;font:inherit;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--ng-cyan);background:rgba(62,240,255,.06);
  border:2px solid var(--ng-cyan);border-radius:12px;cursor:pointer;box-shadow:0 0 22px -6px var(--ng-cyan),inset 0 0 14px -8px var(--ng-cyan);transition:background-color .2s,color .2s}
.ng-open:hover{color:var(--ng-bg);background:var(--ng-cyan)}
.ng-facts{display:flex;flex-wrap:wrap;justify-content:center;gap:10px 26px;margin:28px 0 0;padding:0;list-style:none;font-size:.88rem;color:var(--ng-muted)}
.ng-facts li{display:inline-flex;align-items:center;gap:8px}
.ng-facts li::before{content:"";width:6px;height:6px;transform:rotate(45deg);background:var(--ng-pink);box-shadow:0 0 8px var(--ng-pink)}
/* scene: sun on the horizon and a moving grid floor */
.ng-scene{position:absolute;inset:auto 0 0;height:24vh;z-index:-1;pointer-events:none}
.ng-sun{position:absolute;left:50%;bottom:100%;width:min(240px,60vw);aspect-ratio:1;margin-bottom:max(-96px,-24vw);border-radius:50%;transform:translateX(-50%);opacity:.9;
  background:linear-gradient(#ffd36e 10%,#ff4fd8 75%);
  -webkit-mask:linear-gradient(#000 40%,transparent 40% 43%,#000 43% 50%,transparent 50% 54%,#000 54% 60%,transparent 60% 65%,#000 65%);
  mask:linear-gradient(#000 40%,transparent 40% 43%,#000 43% 50%,transparent 50% 54%,#000 54% 60%,transparent 60% 65%,#000 65%);
  filter:drop-shadow(0 0 30px rgba(255,79,216,.5))}
.ng-floor{position:absolute;inset:0;z-index:0;overflow:hidden;background:var(--ng-bg);box-shadow:0 -1px 0 var(--ng-pink),0 -8px 30px -4px rgba(255,79,216,.6)}
.ng-floor::after{content:"";position:absolute;inset:0;background:linear-gradient(var(--ng-bg),transparent 35%)}
.ng-floor::before{content:"";position:absolute;inset:0 -50% -40%;transform:perspective(320px) rotateX(58deg);transform-origin:50% 0;
  background:linear-gradient(rgba(255,79,216,.7) 2px,transparent 2px) 0 0/60px 60px,linear-gradient(90deg,rgba(255,79,216,.7) 2px,transparent 2px) 0 0/60px 60px;
  animation:ng-drive 1.2s linear infinite;z-index:-1}
@keyframes ng-drive{to{background-position:0 60px,0 60px}}
/* popover */
.ng-pop{width:min(440px,calc(100% - 32px));padding:30px;color:var(--ng-ink);background:var(--ng-panel);border:1px solid var(--ng-pink);border-radius:20px;
  box-shadow:0 0 0 1px rgba(255,79,216,.25),0 0 60px -10px rgba(255,79,216,.55);
  opacity:0;transform:translateY(14px) scale(.97);transition:opacity .25s,transform .25s,overlay .25s allow-discrete,display .25s allow-discrete}
.ng-pop:popover-open{opacity:1;transform:none}
@starting-style{.ng-pop:popover-open{opacity:0;transform:translateY(14px) scale(.97)}}
.ng-pop::backdrop{background:rgba(7,5,15,.72);backdrop-filter:blur(4px)}
.ng-pop h2{margin:0 0 8px;padding-right:36px;font-size:1.35rem;letter-spacing:-.01em}
.ng-pop > p{margin:0 0 20px;line-height:1.55;color:var(--ng-muted)}
.ng-close{position:absolute;top:16px;right:16px;display:grid;place-items:center;width:34px;height:34px;padding:0;color:var(--ng-muted);background:transparent;border:1px solid #4b3f6e;border-radius:10px;cursor:pointer}
.ng-close:hover{color:var(--ng-ink);border-color:var(--ng-muted)}
.ng-pop label{display:block;margin:0 0 8px;font-size:.9rem;font-weight:700}
.ng-field{display:flex;flex-direction:column;gap:10px}
.ng-field input{height:50px;padding:0 16px;font:inherit;color:var(--ng-ink);background:#0b0716;border:1px solid #8a7fb3;border-radius:12px}
.ng-field input::placeholder{color:#9a90c0}
.ng-field input[aria-invalid="true"],.ng-field input:user-invalid{border-color:var(--ng-err);box-shadow:0 0 0 1px var(--ng-err)}
.ng-field button{height:50px;font:inherit;font-weight:800;letter-spacing:.04em;color:#12021a;background:var(--ng-pink);border:0;border-radius:12px;cursor:pointer;box-shadow:0 0 24px -6px var(--ng-pink);transition:filter .2s}
.ng-field button:hover{filter:brightness(1.12)}
.ng-err{min-height:1.4em;margin:10px 0 0;font-size:.88rem;color:var(--ng-err)}
.ng-done{padding:16px 18px;border:1px solid rgba(62,240,255,.5);border-radius:14px;background:rgba(62,240,255,.07);line-height:1.5}
.ng-done:focus{outline:none}
.ng-done p{margin:0}
@media (prefers-reduced-motion:reduce){
  .ng-onair::before,.ng-title,.ng-floor::before{animation:none}
  .ng-pop,.ng-open,.ng-field button{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="ng d-flex flex-column overflow-hidden" data-bs-theme="dark">
  <header class="d-flex flex-wrap align-items-center justify-content-between gap-3 small text-body-secondary">
    <a class="ng-logo d-inline-flex align-items-center gap-2 fw-bolder text-uppercase text-body" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="13" cy="13" r="3" fill="#ff4fd8" stroke="none"/><path d="M8 8a7 7 0 0 0 0 10M18 8a7 7 0 0 1 0 10" stroke="#ff4fd8"/><path d="M4.5 4.5a12 12 0 0 0 0 17M21.5 4.5a12 12 0 0 1 0 17" stroke="#3ef0ff"/></svg>Afterglow FM</a>
    <p class="m-0">Online radio · Streaming from 1 November</p>
  </header>
  <section class="ng-body flex-grow-1 d-flex flex-column align-items-center justify-content-center text-center" aria-labelledby="ng-title">
    <p class="ng-onair badge rounded-pill d-inline-flex align-items-center gap-2 text-uppercase mb-3">On air soon</p>
    <h1 class="ng-title text-uppercase mb-3" id="ng-title">Afterglow</h1>
    <p class="ng-lead text-body-secondary mb-4 pb-1">Synthwave, city pop and night-drive records, 24 hours a day, picked by people instead of an algorithm. Tune in when the signal goes live.</p>
    <button class="btn btn-neon text-uppercase" type="button" popovertarget="ng-pop">Get the launch signal</button>
    <ul class="ng-facts list-unstyled d-flex flex-wrap justify-content-center text-body-secondary mt-4 mb-0">
      <li class="d-inline-flex align-items-center gap-2">24/7 stream</li>
      <li class="d-inline-flex align-items-center gap-2">No ads, ever</li>
      <li class="d-inline-flex align-items-center gap-2">Live DJ sets on Fridays</li>
    </ul>
  </section>
  <div class="ng-scene position-absolute" aria-hidden="true"><span class="ng-sun position-absolute rounded-circle"></span><span class="ng-floor position-absolute top-0 start-0 w-100 h-100"></span></div>
  <div class="ng-pop" id="ng-pop" popover role="dialog" aria-labelledby="ng-pop-title">
    <button class="btn-close position-absolute" type="button" popovertarget="ng-pop" popovertargetaction="hide" aria-label="Close"></button>
    <h2 class="fs-5 fw-bold pe-5 mb-2" id="ng-pop-title">Be there when we go on air</h2>
    <p class="text-body-secondary mb-4">One email when the stream starts, with the link and the first week's schedule.</p>
    <form data-signup="ng-done" action="#" method="post">
      <label class="form-label fw-bold small" for="ng-email">Email address</label>
      <div class="d-grid gap-2">
        <input class="form-control" id="ng-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ng-err" autofocus>
        <button class="btn btn-pink" type="submit">Send me the signal</button>
      </div>
      <p class="ng-err small mt-2 mb-0" id="ng-err" data-error aria-live="polite"></p>
    </form>
    <div class="ng-done px-3 py-3" id="ng-done" tabindex="-1" hidden><p class="m-0"><strong>You are on the frequency.</strong> Look out for one email on 1 November.</p></div>
  </div>
</main>
CSS
/* Bootstrap dark colour mode (data-bs-theme="dark"), .btn, .btn-close, .form-control and .badge. The sign-up form lives in a
   native popover rather than a Bootstrap modal, so no Bootstrap JavaScript is loaded: popovertarget opens it, Esc and a click
   outside close it, and @starting-style animates it in. */
.ng{--ng-pink:#ff4fd8;--ng-cyan:#3ef0ff;--ng-panel:#120c22;--ng-err:#ff9bb0;
  --bs-body-bg:#07050f;--bs-body-color:#f5f3ff;--bs-secondary-color:#b8b0d6;
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  position:relative;isolation:isolate;min-height:100dvh;padding:26px clamp(20px,5vw,56px);
  color:var(--bs-body-color);font-family:var(--bs-body-font-family);
  background:radial-gradient(70% 50% at 50% 0%,#2a0f45 0,transparent 70%),var(--bs-body-bg)}
.ng a{color:inherit}
.ng a:focus-visible{outline:2px solid var(--ng-cyan);outline-offset:3px;border-radius:2px;box-shadow:none}
.ng-logo{letter-spacing:.12em;text-decoration:none}
.ng-body{padding:36px 0 calc(24vh + 150px)}
.ng-onair{--bs-badge-font-size:.74rem;--bs-badge-font-weight:800;--bs-badge-padding-x:12px;--bs-badge-padding-y:5px;letter-spacing:.2em;
  color:var(--ng-cyan);border:1px solid rgba(62,240,255,.55);box-shadow:0 0 18px -4px rgba(62,240,255,.6)}
.ng-onair::before{content:"";width:7px;height:7px;border-radius:50%;background:#ff5a7a;box-shadow:0 0 8px #ff5a7a;animation:ng-blink 1.6s ease-in-out infinite}
@keyframes ng-blink{50%{opacity:.25}}
.ng-title{font-size:clamp(3rem,11vw,7.5rem);font-weight:900;line-height:.95;letter-spacing:.04em;color:#fff0fb;
  text-shadow:0 0 6px var(--ng-pink),0 0 22px var(--ng-pink),0 0 48px #b01c9c;animation:ng-flicker 7s infinite}
@keyframes ng-flicker{0%,91%,93%,95%,100%{opacity:1}92%,94%{opacity:.55}}
.ng-lead{max-width:520px;font-size:1.08rem;line-height:1.6}
.ng .btn-neon{--bs-btn-color:var(--ng-cyan);--bs-btn-bg:rgba(62,240,255,.06);--bs-btn-border-color:var(--ng-cyan);--bs-btn-border-width:2px;--bs-btn-border-radius:12px;
  --bs-btn-hover-color:#07050f;--bs-btn-hover-bg:var(--ng-cyan);--bs-btn-hover-border-color:var(--ng-cyan);
  --bs-btn-active-color:#07050f;--bs-btn-active-bg:var(--ng-cyan);--bs-btn-active-border-color:var(--ng-cyan);
  --bs-btn-font-weight:800;--bs-btn-padding-x:28px;height:52px;letter-spacing:.06em;
  box-shadow:0 0 22px -6px var(--ng-cyan),inset 0 0 14px -8px var(--ng-cyan)}
.ng .btn-neon:focus-visible{outline:2px solid var(--ng-cyan);outline-offset:3px;box-shadow:0 0 22px -6px var(--ng-cyan)}
.ng-facts{font-size:.88rem;gap:10px 26px}
.ng-facts li::before{content:"";width:6px;height:6px;transform:rotate(45deg);background:var(--ng-pink);box-shadow:0 0 8px var(--ng-pink)}
.ng-scene{inset:auto 0 0;height:24vh;z-index:-1;pointer-events:none}
.ng-sun{left:50%;bottom:100%;width:min(240px,60vw);aspect-ratio:1;margin-bottom:max(-96px,-24vw);transform:translateX(-50%);opacity:.9;
  background:linear-gradient(#ffd36e 10%,#ff4fd8 75%);
  -webkit-mask:linear-gradient(#000 40%,transparent 40% 43%,#000 43% 50%,transparent 50% 54%,#000 54% 60%,transparent 60% 65%,#000 65%);
  mask:linear-gradient(#000 40%,transparent 40% 43%,#000 43% 50%,transparent 50% 54%,#000 54% 60%,transparent 60% 65%,#000 65%);
  filter:drop-shadow(0 0 30px rgba(255,79,216,.5))}
.ng-floor{z-index:0;overflow:hidden;background:var(--bs-body-bg);box-shadow:0 -1px 0 var(--ng-pink),0 -8px 30px -4px rgba(255,79,216,.6)}
.ng-floor::after{content:"";position:absolute;inset:0;background:linear-gradient(var(--bs-body-bg),transparent 35%)}
.ng-floor::before{content:"";position:absolute;inset:0 -50% -40%;transform:perspective(320px) rotateX(58deg);transform-origin:50% 0;
  background:linear-gradient(rgba(255,79,216,.7) 2px,transparent 2px) 0 0/60px 60px,linear-gradient(90deg,rgba(255,79,216,.7) 2px,transparent 2px) 0 0/60px 60px;
  animation:ng-drive 1.2s linear infinite;z-index:-1}
@keyframes ng-drive{to{background-position:0 60px,0 60px}}
.ng-pop{--bs-body-color:#f5f3ff;width:min(440px,calc(100% - 32px));padding:30px;color:var(--bs-body-color);background:var(--ng-panel);border:1px solid var(--ng-pink);border-radius:20px;
  box-shadow:0 0 0 1px rgba(255,79,216,.25),0 0 60px -10px rgba(255,79,216,.55);
  opacity:0;transform:translateY(14px) scale(.97);transition:opacity .25s,transform .25s,overlay .25s allow-discrete,display .25s allow-discrete}
.ng-pop:popover-open{opacity:1;transform:none}
@starting-style{.ng-pop:popover-open{opacity:0;transform:translateY(14px) scale(.97)}}
.ng-pop::backdrop{background:rgba(7,5,15,.72);backdrop-filter:blur(4px)}
.ng-pop .btn-close{top:20px;right:20px;--bs-btn-close-focus-shadow:0 0 0 2px var(--ng-cyan)}
.ng .form-control{height:50px;padding:0 16px;background:#0b0716;border-color:#8a7fb3;border-radius:12px}
.ng .form-control::placeholder{color:#9a90c0}
.ng .form-control:focus{border-color:#8a7fb3;box-shadow:none;outline:2px solid var(--ng-cyan);outline-offset:3px}
.ng .form-control[aria-invalid="true"],.ng .form-control:user-invalid{border-color:var(--ng-err);box-shadow:0 0 0 1px var(--ng-err)}
.ng .btn-pink{--bs-btn-color:#12021a;--bs-btn-bg:var(--ng-pink);--bs-btn-border-color:var(--ng-pink);--bs-btn-hover-color:#12021a;--bs-btn-hover-bg:#ff6fe0;--bs-btn-hover-border-color:#ff6fe0;
  --bs-btn-active-color:#12021a;--bs-btn-active-bg:#ff6fe0;--bs-btn-active-border-color:#ff6fe0;--bs-btn-font-weight:800;--bs-btn-border-radius:12px;height:50px;letter-spacing:.04em;box-shadow:0 0 24px -6px var(--ng-pink)}
.ng .btn-pink:focus-visible{outline:2px solid var(--ng-cyan);outline-offset:3px}
.ng-err{min-height:1.4em;color:var(--ng-err)}
.ng-done{border:1px solid rgba(62,240,255,.5);border-radius:14px;background:rgba(62,240,255,.07)}
.ng-done:focus{outline:none}
@media (prefers-reduced-motion:reduce){
  .ng-onair::before,.ng-title,.ng-floor::before{animation:none}
  .ng-pop,.ng .btn{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="relative isolate flex min-h-dvh flex-col overflow-hidden bg-[radial-gradient(70%_50%_at_50%_0%,#2a0f45_0,transparent_70%)] bg-ng-bg px-[clamp(20px,5vw,56px)] py-[26px] font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] text-ng-ink">
  <header class="flex flex-wrap items-center justify-between gap-3 text-[.85rem] text-ng-muted">
    <a class="inline-flex items-center gap-[9px] font-extrabold tracking-[.12em] text-ng-ink uppercase focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ng-cyan" href="#"><svg width="26" height="26" viewBox="0 0 26 26" fill="none" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="13" cy="13" r="3" fill="#ff4fd8" stroke="none"/><path d="M8 8a7 7 0 0 0 0 10M18 8a7 7 0 0 1 0 10" stroke="#ff4fd8"/><path d="M4.5 4.5a12 12 0 0 0 0 17M21.5 4.5a12 12 0 0 1 0 17" stroke="#3ef0ff"/></svg>Afterglow FM</a>
    <p>Online radio · Streaming from 1 November</p>
  </header>
  <section class="flex flex-1 flex-col items-center justify-center pt-9 pb-[calc(24vh+150px)] text-center" aria-labelledby="ng-title">
    <p class="mb-[18px] inline-flex items-center gap-2 rounded-full border border-[rgba(62,240,255,.55)] px-3 py-[5px] text-[.74rem] font-extrabold tracking-[.2em] text-ng-cyan uppercase shadow-[0_0_18px_-4px_rgba(62,240,255,.6)] before:size-[7px] before:animate-ng-blink before:rounded-full before:bg-[#ff5a7a] before:shadow-[0_0_8px_#ff5a7a] motion-reduce:before:animate-none">On air soon</p>
    <h1 class="mb-3.5 animate-ng-flicker text-[clamp(3rem,11vw,7.5rem)] leading-[.95] font-black tracking-[.04em] text-[#fff0fb] uppercase [text-shadow:0_0_6px_#ff4fd8,0_0_22px_#ff4fd8,0_0_48px_#b01c9c] motion-reduce:animate-none" id="ng-title">Afterglow</h1>
    <p class="mb-[30px] max-w-[520px] text-[1.08rem] leading-[1.6] text-ng-muted">Synthwave, city pop and night-drive records, 24 hours a day, picked by people instead of an algorithm. Tune in when the signal goes live.</p>
    <button class="h-[52px] cursor-pointer rounded-xl border-2 border-ng-cyan bg-[rgba(62,240,255,.06)] px-7 font-extrabold tracking-[.06em] text-ng-cyan uppercase shadow-[0_0_22px_-6px_#3ef0ff,inset_0_0_14px_-8px_#3ef0ff] transition-colors duration-200 hover:bg-ng-cyan hover:text-ng-bg focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ng-cyan motion-reduce:transition-none" type="button" popovertarget="ng-pop">Get the launch signal</button>
    <ul class="mt-7 flex flex-wrap justify-center gap-x-[26px] gap-y-2.5 text-[.88rem] text-ng-muted">
      <li class="inline-flex items-center gap-2 before:size-1.5 before:rotate-45 before:bg-ng-pink before:shadow-[0_0_8px_#ff4fd8]">24/7 stream</li>
      <li class="inline-flex items-center gap-2 before:size-1.5 before:rotate-45 before:bg-ng-pink before:shadow-[0_0_8px_#ff4fd8]">No ads, ever</li>
      <li class="inline-flex items-center gap-2 before:size-1.5 before:rotate-45 before:bg-ng-pink before:shadow-[0_0_8px_#ff4fd8]">Live DJ sets on Fridays</li>
    </ul>
  </section>
  <div class="pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-[24vh]" aria-hidden="true">
    <span class="absolute bottom-full left-1/2 mb-[max(-96px,-24vw)] aspect-square w-[min(240px,60vw)] -translate-x-1/2 rounded-full bg-[linear-gradient(#ffd36e_10%,#ff4fd8_75%)] opacity-90 drop-shadow-[0_0_30px_rgba(255,79,216,.5)] [mask:linear-gradient(#000_40%,transparent_40%_43%,#000_43%_50%,transparent_50%_54%,#000_54%_60%,transparent_60%_65%,#000_65%)]"></span>
    <span class="absolute inset-0 overflow-hidden bg-ng-bg shadow-[0_-1px_0_#ff4fd8,0_-8px_30px_-4px_rgba(255,79,216,.6)]"><span class="absolute inset-[0_-50%_-40%] origin-top animate-ng-drive bg-[linear-gradient(rgba(255,79,216,.7)_2px,transparent_2px),linear-gradient(90deg,rgba(255,79,216,.7)_2px,transparent_2px)] bg-size-[60px_60px] [transform:perspective(320px)_rotateX(58deg)] motion-reduce:animate-none"></span></span>
    <span class="absolute inset-0 bg-[linear-gradient(#07050f,transparent_35%)]"></span>
  </div>
  <div class="m-auto w-[min(440px,calc(100%-32px))] translate-y-3.5 scale-97 rounded-[20px] border border-ng-pink bg-ng-panel p-[30px] text-ng-ink opacity-0 shadow-[0_0_0_1px_rgba(255,79,216,.25),0_0_60px_-10px_rgba(255,79,216,.55)] transition-[opacity,translate,scale,overlay,display] transition-discrete duration-250 backdrop:bg-[rgba(7,5,15,.72)] backdrop:backdrop-blur-[4px] open:translate-y-0 open:scale-100 open:opacity-100 starting:open:translate-y-3.5 starting:open:scale-97 starting:open:opacity-0 motion-reduce:transition-none" id="ng-pop" popover role="dialog" aria-labelledby="ng-pop-title">
    <button class="absolute top-4 right-4 grid size-[34px] cursor-pointer place-items-center rounded-[10px] border border-[#4b3f6e] text-ng-muted hover:border-ng-muted hover:text-ng-ink focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ng-cyan" type="button" popovertarget="ng-pop" popovertargetaction="hide" aria-label="Close"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
    <h2 class="mb-2 pr-9 text-[1.35rem] font-bold tracking-[-.01em]" id="ng-pop-title">Be there when we go on air</h2>
    <p class="mb-5 leading-[1.55] text-ng-muted">One email when the stream starts, with the link and the first week's schedule.</p>
    <form data-signup="ng-done" action="#" method="post">
      <label class="mb-2 block text-[.9rem] font-bold" for="ng-email">Email address</label>
      <div class="flex flex-col gap-2.5">
        <input class="h-[50px] rounded-xl border border-[#8a7fb3] bg-[#0b0716] px-4 text-ng-ink placeholder:text-[#9a90c0] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ng-cyan aria-invalid:border-ng-err aria-invalid:shadow-[0_0_0_1px_#ff9bb0] user-invalid:border-ng-err user-invalid:shadow-[0_0_0_1px_#ff9bb0]" id="ng-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ng-err" autofocus>
        <button class="h-[50px] cursor-pointer rounded-xl bg-ng-pink font-extrabold tracking-[.04em] text-[#12021a] shadow-[0_0_24px_-6px_#ff4fd8] transition-[filter] duration-200 hover:brightness-112 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ng-cyan motion-reduce:transition-none" type="submit">Send me the signal</button>
      </div>
      <p class="mt-2.5 min-h-[1.4em] text-[.88rem] text-ng-err" id="ng-err" data-error aria-live="polite"></p>
    </form>
    <div class="rounded-[14px] border border-[rgba(62,240,255,.5)] bg-[rgba(62,240,255,.07)] px-[18px] py-4 leading-normal focus:outline-none" id="ng-done" tabindex="-1" hidden><p><strong>You are on the frequency.</strong> Look out for one email on 1 November.</p></div>
  </div>
</main>
CSS
/* The sign-up form lives in a native popover: popovertarget opens it, Esc and a click outside close it,
   and the starting: variant (@starting-style) animates it in. No JavaScript is needed to open or close it. */
@theme {
  --color-ng-bg: #07050f;
  --color-ng-ink: #f5f3ff;
  --color-ng-muted: #b8b0d6;
  --color-ng-pink: #ff4fd8;
  --color-ng-cyan: #3ef0ff;
  --color-ng-panel: #120c22;
  --color-ng-err: #ff9bb0;
  --animate-ng-blink: ng-blink 1.6s ease-in-out infinite;
  --animate-ng-flicker: ng-flicker 7s infinite;
  --animate-ng-drive: ng-drive 1.2s linear infinite;
  @keyframes ng-blink { 50% { opacity: .25; } }
  @keyframes ng-flicker { 0%, 91%, 93%, 95%, 100% { opacity: 1; } 92%, 94% { opacity: .55; } }
  @keyframes ng-drive { to { background-position: 0 60px, 0 60px; } }
}
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 dark synthwave coming soon page for an online radio station, and the modern-CSS set in this pack. A glowing magenta wordmark with a slow neon flicker sits above a cyan On air soon tag, a short pitch and three facts, and below them a striped retro sun sinks behind a pink perspective grid that scrolls towards the viewer. The Get the launch signal button opens the sign-up form in a native popover, the kind of modal that usually needs a JavaScript library. Here popovertarget opens it, a close button with popovertargetaction closes it, and Escape or a click on the dimmed backdrop dismisses it, all handled by the browser. @starting-style and transition-behavior: allow-discrete fade and lift it in and out, ::backdrop blurs the page behind, and :user-invalid outlines the field after the visitor has typed a bad address. The only script is the shared sign-up handler, which writes the error message and shows the confirmation; nothing is sent. The sun, grid and flicker are decorative, hidden from assistive technology and still under reduced motion.

What’s included

  • Signup modal built on the native popover attribute, no JavaScript to open or close
  • @starting-style entry and allow-discrete exit transitions
  • Neon wordmark, striped sun and moving perspective grid in CSS
  • Esc, backdrop click and a labelled close button all dismiss it
  • All animation stops under prefers-reduced-motion

Accessibility

The popover has role="dialog" and is named by its heading; the input has autofocus so focus lands in the form when it opens, Esc closes it, and the close button has an aria-label. The field has a visible label, an aria-live error linked with aria-describedby and aria-invalid when wrong. Cyan focus outlines are used throughout, all text meets 4.5:1 on the near-black ground, the scene is aria-hidden, and the flicker, blink, grid motion and popover transitions are removed under prefers-reduced-motion.

Customise it

The palette is --ng-pink, --ng-cyan, --ng-bg and --ng-panel on .ng (plain CSS and Bootstrap) or the --color-ng-* theme colours in Tailwind. Grid speed is the ng-drive animation duration and the glow is the text-shadow on the headline.