Open demo

CSS code

HTML
<div class="demo-grid">
<div><span class="lbl">Classic card</span>
<form class="su" action="#" method="post" data-signup aria-labelledby="fn-title">
  <div class="su-brand"><span class="su-mark"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z"/><path d="M9 9h6M9 13h4"/></svg></span>Fieldnotes</div>
  <h2 class="su-title" id="fn-title">Start your journal</h2>
  <p class="su-sub">Free for your first 100 entries. No card needed.</p>
  <p class="su-note"><span class="su-req" aria-hidden="true">*</span> Required field</p>
  <div class="su-field">
    <label class="su-label" for="fn-name">Full name <span class="su-req" aria-hidden="true">*</span></label>
    <input class="su-input" id="fn-name" name="name" type="text" autocomplete="name" required aria-describedby="fn-name-err" data-missing="Enter your name.">
    <p class="su-err" id="fn-name-err"></p>
  </div>
  <div class="su-field">
    <label class="su-label" for="fn-email">Email <span class="su-req" aria-hidden="true">*</span></label>
    <input class="su-input" id="fn-email" name="email" type="email" autocomplete="email" required aria-describedby="fn-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
    <p class="su-err" id="fn-email-err"></p>
  </div>
  <div class="su-field">
    <label class="su-label" for="fn-pw">Password <span class="su-req" aria-hidden="true">*</span></label>
    <div class="su-pw">
      <input class="su-input" id="fn-pw" name="password" type="password" autocomplete="new-password" minlength="8" required aria-describedby="fn-pw-hint fn-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
      <button class="su-eye" type="button" aria-controls="fn-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="su-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="su-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <p class="su-hint" id="fn-pw-hint">At least 8 characters.</p>
    <p class="su-err" id="fn-pw-err"></p>
  </div>
  <div class="su-check-wrap">
    <label class="su-check"><input id="fn-terms" name="terms" type="checkbox" required aria-describedby="fn-terms-err" data-missing="Tick the box to accept the terms."><span>I agree to the <a href="#">Terms</a> and <a href="#">Privacy Policy</a> <span class="su-req" aria-hidden="true">*</span></span></label>
    <p class="su-err" id="fn-terms-err"></p>
  </div>
  <button class="su-btn" type="submit">Create account</button>
  <p class="su-status" role="status" data-status></p>
  <p class="su-foot">Already have an account? <a href="#">Log in</a></p>
</form>
</div>
<div><span class="lbl">Floating labels</span>
<form class="su su--green" action="#" method="post" data-signup aria-labelledby="cb-title">
  <div class="su-brand"><span class="su-mark"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16l-1.5 11a2 2 0 0 1-2 1.7h-9a2 2 0 0 1-2-1.7z"/><path d="M9 7a3 3 0 0 1 6 0"/></svg></span>Cobble</div>
  <h2 class="su-title" id="cb-title">One list for the whole house</h2>
  <p class="su-sub">Shared grocery lists that update while you shop.</p>
  <p class="su-note"><span class="su-req" aria-hidden="true">*</span> Required field</p>
  <div class="su-row">
    <div class="su-field su-fl">
      <input class="su-input" id="cb-first" name="given-name" type="text" autocomplete="given-name" placeholder=" " required aria-describedby="cb-first-err" data-missing="Enter your first name.">
      <label for="cb-first">First name <span class="su-req" aria-hidden="true">*</span></label>
      <p class="su-err" id="cb-first-err"></p>
    </div>
    <div class="su-field su-fl">
      <input class="su-input" id="cb-last" name="family-name" type="text" autocomplete="family-name" placeholder=" ">
      <label for="cb-last">Last name</label>
    </div>
  </div>
  <div class="su-field su-fl">
    <input class="su-input" id="cb-email" name="email" type="email" autocomplete="email" placeholder=" " required aria-describedby="cb-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
    <label for="cb-email">Email <span class="su-req" aria-hidden="true">*</span></label>
    <p class="su-err" id="cb-email-err"></p>
  </div>
  <div class="su-field">
    <div class="su-pw su-fl">
      <input class="su-input" id="cb-pw" name="password" type="password" autocomplete="new-password" minlength="8" placeholder=" " required aria-describedby="cb-pw-hint cb-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
      <label for="cb-pw">Password <span class="su-req" aria-hidden="true">*</span></label>
      <button class="su-eye" type="button" aria-controls="cb-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="su-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="su-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <p class="su-hint" id="cb-pw-hint">At least 8 characters.</p>
    <p class="su-err" id="cb-pw-err"></p>
  </div>
  <label class="su-check"><input id="cb-tips" name="tips" type="checkbox"><span>Send me a weekly meal-plan email (optional)</span></label>
  <p class="su-terms">By creating an account you agree to the <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.</p>
  <button class="su-btn" type="submit">Create free account</button>
  <p class="su-status" role="status" data-status></p>
  <p class="su-foot">Already have an account? <a href="#">Log in</a></p>
</form>
</div>
</div>
CSS
/* Simple sign-up cards. Native inputs with visible labels; the eye button only appears once the script runs. */
.su{--su-accent:#9a3412;--su-accent-dark:#7c2d12;--su-ring:rgba(154,52,18,.22);--su-ink:#1c1917;--su-muted:#57534e;
  --su-line:#8a817a;--su-bg:#fff;--su-error:#b91c1c;--su-ok-bg:#f0fdf4;--su-ok:#166534;
  background:var(--su-bg);color:var(--su-ink);border-radius:18px;padding:34px 32px 28px;font-size:.92rem;line-height:1.45;
  box-shadow:0 1px 2px rgba(28,25,23,.06),0 14px 34px -14px rgba(28,25,23,.22)}
.su--green{--su-accent:#166534;--su-accent-dark:#14532d;--su-ring:rgba(22,101,52,.22)}
.su-brand{display:flex;align-items:center;gap:10px;font-weight:700;margin-bottom:22px}
.su-mark{width:32px;height:32px;border-radius:9px;background:var(--su-accent);color:#fff;display:grid;place-items:center}
.su-title{font-family:Georgia,"Times New Roman",serif;font-size:1.62rem;font-weight:400;line-height:1.15;letter-spacing:-.01em}
.su-sub{color:var(--su-muted);margin:6px 0 20px}
.su-note{font-size:.8rem;color:var(--su-muted);margin-bottom:14px}
.su-req{color:var(--su-accent);font-weight:700}
.su-row{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
.su-field{margin-bottom:16px;min-width:0}
.su-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.su-input{display:block;width:100%;font:inherit;color:inherit;background:#fff;border:1px solid var(--su-line);border-radius:10px;
  padding:11px 13px;transition:border-color .15s,box-shadow .15s}
.su-input:focus{outline:none;border-color:var(--su-accent);box-shadow:0 0 0 3px var(--su-ring)}
/* password: input and eye button share one border */
.su-pw{display:flex;background:#fff;border:1px solid var(--su-line);border-radius:10px;transition:border-color .15s,box-shadow .15s}
.su-pw:focus-within{border-color:var(--su-accent);box-shadow:0 0 0 3px var(--su-ring)}
.su-pw .su-input,.su-pw .su-input:focus{flex:1;min-width:0;border:0;box-shadow:none;background:transparent}
.su-eye{flex:none;width:46px;border:0;background:none;color:var(--su-muted);cursor:pointer;border-radius:0 10px 10px 0;display:grid;place-items:center}
.su-eye:hover{color:var(--su-ink)}
.su-eye:focus-visible{outline:2px solid var(--su-accent);outline-offset:-5px;border-radius:8px}
.su-eye[hidden],.su-eye[aria-pressed="true"] .su-show,.su-eye[aria-pressed="false"] .su-hide{display:none}
.su-hint{font-size:.8rem;color:var(--su-muted);margin-top:6px}
/* error state, set by the script */
.su-input.is-invalid,.su-pw:has(.is-invalid){border-color:var(--su-error)}
.su-input.is-invalid:focus,.su-pw:has(.is-invalid):focus-within{box-shadow:0 0 0 3px rgba(185,28,28,.2)}
.su-err{display:flex;gap:6px;align-items:flex-start;color:var(--su-error);font-size:.8rem;font-weight:600;margin-top:6px}
.su-err::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--su-error);color:#fff;
  font-size:.7rem;font-weight:800;display:grid;place-items:center}
.su-err:empty{display:none}
/* checkbox drawn on the native input */
.su-check{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;margin:4px 0 18px;cursor:pointer}
.su-check input{appearance:none;flex:none;width:18px;height:18px;margin-top:1px;border:1.5px solid var(--su-line);border-radius:5px;
  background:#fff center/12px no-repeat;cursor:pointer;transition:background-color .15s,border-color .15s}
.su-check input:checked{background-color:var(--su-accent);border-color:var(--su-accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.su-check input:focus-visible{outline:2px solid var(--su-accent);outline-offset:2px}
.su-check input.is-invalid{border-color:var(--su-error)}
.su-check-wrap .su-err{margin:-12px 0 16px 28px}
.su a{color:var(--su-accent);text-decoration:underline;text-underline-offset:2px}
.su a:hover{color:var(--su-accent-dark)}
.su a:focus-visible{outline:2px solid var(--su-accent);outline-offset:2px;border-radius:3px}
.su-btn{display:block;width:100%;font:inherit;font-weight:600;color:#fff;background:var(--su-accent);border:0;border-radius:10px;padding:12px 16px;
  cursor:pointer;transition:background-color .15s}
.su-btn:hover{background:var(--su-accent-dark)}
.su-btn:focus-visible{outline:2px solid var(--su-accent);outline-offset:3px}
.su-status:not(:empty){margin-top:14px;padding:10px 12px;border-radius:10px;background:var(--su-ok-bg);color:var(--su-ok);font-weight:600;font-size:.84rem}
.su-terms{font-size:.78rem;color:var(--su-muted);margin:2px 0 16px}
.su-foot{margin-top:18px;text-align:center;color:var(--su-muted);font-size:.86rem}
/* floating label: the label sits inside the field and rises when it has focus or a value */
.su-fl{position:relative}
.su-fl .su-input{padding:21px 13px 7px}
.su-fl label{position:absolute;left:14px;top:14px;font-size:.92rem;color:var(--su-muted);pointer-events:none;
  transform-origin:0 0;transition:transform .15s,color .15s}
.su-fl .su-input:focus~label,.su-fl .su-input:not(:placeholder-shown)~label{transform:translateY(-8px) scale(.78);color:var(--su-ink);font-weight:600}
.su-fl.su-pw .su-input{padding-right:6px}
@media (prefers-reduced-motion:reduce){.su-fl label{transition:none}}
@media (max-width:420px){.su{padding:26px 20px 22px}.su-row{grid-template-columns:1fr;gap:0}}
@media (prefers-reduced-motion:reduce){.su-input,.su-pw,.su-check input,.su-btn{transition:none}}
JavaScript
// Show/hide password, and inline errors on submit. No libraries. Without the script the eye button
// stays hidden and the browser's own validation still checks the required fields.
document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
  var input = document.getElementById(btn.getAttribute('aria-controls'));
  btn.hidden = false;
  btn.addEventListener('click', function () {
    var show = input.type === 'password';
    input.type = show ? 'text' : 'password';
    btn.setAttribute('aria-pressed', show ? 'true' : 'false');
  });
});
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var status = form.querySelector('[data-status]');
  form.noValidate = true;
  function check(input) {
    var v = input.validity, msg = v.valid ? '' : v.valueMissing ? input.dataset.missing : (input.dataset.bad || input.validationMessage);
    var out = document.getElementById(input.id + '-err');
    input.classList.toggle('is-invalid', !!msg);
    if (msg) input.setAttribute('aria-invalid', 'true'); else input.removeAttribute('aria-invalid');
    if (out) out.textContent = msg;
    return !msg;
  }
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent
    var bad = Array.prototype.filter.call(form.querySelectorAll('[required]'), function (f) { return !check(f); });
    status.textContent = '';
    if (bad.length) { bad[0].focus(); return; }
    status.textContent = 'Account created. We sent a confirmation link to ' + form.elements.email.value + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="demo-grid">
<div><span class="lbl">Classic card</span>
<form class="card su" action="#" method="post" data-signup aria-labelledby="fn-title">
  <div class="su-brand"><span class="su-mark"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z"/><path d="M9 9h6M9 13h4"/></svg></span>Fieldnotes</div>
  <h2 class="su-title" id="fn-title">Start your journal</h2>
  <p class="su-sub">Free for your first 100 entries. No card needed.</p>
  <p class="su-note"><span class="su-req" aria-hidden="true">*</span> Required field</p>
  <div class="mb-3">
    <label class="form-label" for="fn-name">Full name <span class="su-req" aria-hidden="true">*</span></label>
    <input class="form-control" id="fn-name" name="name" type="text" autocomplete="name" required aria-describedby="fn-name-err" data-missing="Enter your name.">
    <div class="invalid-feedback" id="fn-name-err"></div>
  </div>
  <div class="mb-3">
    <label class="form-label" for="fn-email">Email <span class="su-req" aria-hidden="true">*</span></label>
    <input class="form-control" id="fn-email" name="email" type="email" autocomplete="email" required aria-describedby="fn-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
    <div class="invalid-feedback" id="fn-email-err"></div>
  </div>
  <div class="mb-3">
    <label class="form-label" for="fn-pw">Password <span class="su-req" aria-hidden="true">*</span></label>
    <div class="input-group">
      <input class="form-control" id="fn-pw" name="password" type="password" autocomplete="new-password" minlength="8" required aria-describedby="fn-pw-hint fn-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
      <button class="btn su-eye" type="button" aria-controls="fn-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="su-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="su-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <div class="form-text" id="fn-pw-hint">At least 8 characters.</div>
    <div class="invalid-feedback" id="fn-pw-err"></div>
  </div>
  <div class="su-check-wrap">
    <div class="form-check">
      <input class="form-check-input" id="fn-terms" name="terms" type="checkbox" required aria-describedby="fn-terms-err" data-missing="Tick the box to accept the terms.">
      <label class="form-check-label" for="fn-terms">I agree to the <a href="#">Terms</a> and <a href="#">Privacy Policy</a> <span class="su-req" aria-hidden="true">*</span></label>
    </div>
    <div class="invalid-feedback" id="fn-terms-err"></div>
  </div>
  <button class="btn btn-primary w-100" type="submit">Create account</button>
  <p class="su-status" role="status" data-status></p>
  <p class="su-foot">Already have an account? <a href="#">Log in</a></p>
</form>
</div>
<div><span class="lbl">Floating labels</span>
<form class="card su su--green" action="#" method="post" data-signup aria-labelledby="cb-title">
  <div class="su-brand"><span class="su-mark"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16l-1.5 11a2 2 0 0 1-2 1.7h-9a2 2 0 0 1-2-1.7z"/><path d="M9 7a3 3 0 0 1 6 0"/></svg></span>Cobble</div>
  <h2 class="su-title" id="cb-title">One list for the whole house</h2>
  <p class="su-sub">Shared grocery lists that update while you shop.</p>
  <p class="su-note"><span class="su-req" aria-hidden="true">*</span> Required field</p>
  <div class="row gx-3">
    <div class="col-sm-6 mb-3">
      <div class="form-floating">
        <input class="form-control" id="cb-first" name="given-name" type="text" autocomplete="given-name" placeholder=" " required aria-describedby="cb-first-err" data-missing="Enter your first name.">
        <label for="cb-first">First name <span class="su-req" aria-hidden="true">*</span></label>
      </div>
      <div class="invalid-feedback" id="cb-first-err"></div>
    </div>
    <div class="col-sm-6 mb-3">
      <div class="form-floating">
        <input class="form-control" id="cb-last" name="family-name" type="text" autocomplete="family-name" placeholder=" ">
        <label for="cb-last">Last name</label>
      </div>
    </div>
  </div>
  <div class="mb-3">
    <div class="form-floating">
      <input class="form-control" id="cb-email" name="email" type="email" autocomplete="email" placeholder=" " required aria-describedby="cb-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
      <label for="cb-email">Email <span class="su-req" aria-hidden="true">*</span></label>
    </div>
    <div class="invalid-feedback" id="cb-email-err"></div>
  </div>
  <div class="mb-3">
    <div class="input-group">
      <div class="form-floating">
        <input class="form-control" id="cb-pw" name="password" type="password" autocomplete="new-password" minlength="8" placeholder=" " required aria-describedby="cb-pw-hint cb-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
        <label for="cb-pw">Password <span class="su-req" aria-hidden="true">*</span></label>
      </div>
      <button class="btn su-eye" type="button" aria-controls="cb-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="su-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="su-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <div class="form-text" id="cb-pw-hint">At least 8 characters.</div>
    <div class="invalid-feedback" id="cb-pw-err"></div>
  </div>
  <div class="form-check">
    <input class="form-check-input" id="cb-tips" name="tips" type="checkbox">
    <label class="form-check-label" for="cb-tips">Send me a weekly meal-plan email (optional)</label>
  </div>
  <p class="su-terms">By creating an account you agree to the <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.</p>
  <button class="btn btn-primary w-100" type="submit">Create free account</button>
  <p class="su-status" role="status" data-status></p>
  <p class="su-foot">Already have an account? <a href="#">Log in</a></p>
</form>
</div>
</div>
CSS
/* Bootstrap .card, .form-control, .input-group, .form-floating and .form-check, recoloured through
   Bootstrap's CSS variables. The eye button only appears once the script runs. */
.su{--su-accent:#9a3412;--su-accent-dark:#7c2d12;--su-ring:rgba(154,52,18,.22);--su-ink:#1c1917;--su-muted:#57534e;
  --su-line:#8a817a;--su-error:#b91c1c;
  --bs-card-border-width:0;--bs-card-border-radius:18px;--bs-card-spacer-y:0;--bs-card-spacer-x:0;
  --bs-border-color:var(--su-line);--bs-border-radius:10px;--bs-body-color:var(--su-ink);--bs-secondary-color:var(--su-muted);
  --bs-form-invalid-border-color:var(--su-error);--bs-form-invalid-color:var(--su-error);
  --bs-link-color-rgb:154,52,18;--bs-link-hover-color-rgb:124,45,18;
  padding:34px 32px 28px;color:var(--su-ink);font-size:.92rem;line-height:1.45;
  box-shadow:0 1px 2px rgba(28,25,23,.06),0 14px 34px -14px rgba(28,25,23,.22)}
.su--green{--su-accent:#166534;--su-accent-dark:#14532d;--su-ring:rgba(22,101,52,.22);--bs-link-color-rgb:22,101,52;--bs-link-hover-color-rgb:20,83,45}
.su p{margin:0}
.su .su-brand{display:flex;align-items:center;gap:10px;font-weight:700;margin-bottom:22px}
.su .su-mark{width:32px;height:32px;border-radius:9px;background:var(--su-accent);color:#fff;display:grid;place-items:center}
.su .su-title{font-family:Georgia,"Times New Roman",serif;font-size:1.62rem;font-weight:400;line-height:1.15;letter-spacing:-.01em;margin:0}
.su .su-sub{color:var(--su-muted);margin:6px 0 20px}
.su .su-note{font-size:.8rem;color:var(--su-muted);margin-bottom:14px}
.su .su-req{color:var(--su-accent);font-weight:700}
.su .form-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.su .form-control{font-size:inherit;line-height:inherit;color:var(--su-ink);padding:11px 13px;transition:border-color .15s,box-shadow .15s}
.su .form-control:focus{border-color:var(--su-accent);box-shadow:0 0 0 3px var(--su-ring)}
.su .form-control.is-invalid{background-image:none;padding-right:13px;border-color:var(--su-error)}
.su .input-group .form-control.is-invalid{border:0}
.su .form-control.is-invalid:focus{box-shadow:0 0 0 3px rgba(185,28,28,.2)}
/* password: input and eye button share one border */
.su .input-group{background:#fff;border:1px solid var(--su-line);border-radius:10px;transition:border-color .15s,box-shadow .15s}
.su .input-group:focus-within{border-color:var(--su-accent);box-shadow:0 0 0 3px var(--su-ring)}
.su .input-group:has(.is-invalid){border-color:var(--su-error)}
.su .input-group .form-control,.su .input-group .form-control:focus{border:0;box-shadow:none;background:transparent}
.su .su-eye{width:46px;padding:0;border:0;color:var(--su-muted);border-radius:0 10px 10px 0!important;display:grid;place-items:center}
.su .su-eye:hover{color:var(--su-ink)}
.su .su-eye:focus-visible{box-shadow:none;outline:2px solid var(--su-accent);outline-offset:-5px;border-radius:8px!important}
.su .su-eye[hidden],.su .su-eye[aria-pressed="true"] .su-show,.su .su-eye[aria-pressed="false"] .su-hide{display:none}
.su .form-text{font-size:.8rem;color:var(--su-muted);margin-top:6px}
.su .invalid-feedback{display:flex;gap:6px;align-items:flex-start;font-size:.8rem;font-weight:600;margin-top:6px}
.su .invalid-feedback::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--su-error);color:#fff;
  font-size:.7rem;font-weight:800;display:grid;place-items:center}
.su .invalid-feedback:empty{display:none}
/* floating labels, resized to the same 50px field */
.su .form-floating>.form-control{height:50px;min-height:50px;padding:21px 13px 7px;line-height:1.45}
.su .form-floating>label{padding:14px 14px;font-size:.92rem;color:var(--su-muted);border:0;transition:transform .15s,color .15s}
.su .form-floating>.form-control:focus~label,.su .form-floating>.form-control:not(:placeholder-shown)~label{
  transform:translateY(-8px) scale(.78);color:var(--su-ink);font-weight:600}
.su .form-floating>.form-control:focus~label::after,.su .form-floating>.form-control:not(:placeholder-shown)~label::after{background:none}
/* checkbox */
.su .form-check{display:flex;gap:10px;align-items:flex-start;padding:0;margin:4px 0 18px;min-height:0;font-size:.86rem}
.su .form-check-input{flex:none;float:none;width:18px;height:18px;margin:1px 0 0;border:1.5px solid var(--su-line);border-radius:5px;cursor:pointer;
  --bs-form-check-bg-image:none;transition:background-color .15s,border-color .15s}
.su .form-check-input:checked{background-color:var(--su-accent);border-color:var(--su-accent);background-size:12px;
  --bs-form-check-bg-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.su .form-check-input:focus{box-shadow:none;border-color:var(--su-line)}
.su .form-check-input:checked:focus{border-color:var(--su-accent)}
.su .form-check-input:focus-visible{outline:2px solid var(--su-accent);outline-offset:2px}
.su .form-check-input.is-invalid{border-color:var(--su-error)}
.su .form-check-input.is-invalid~.form-check-label{color:inherit}
.su .form-check-label{cursor:pointer}
.su .su-check-wrap .invalid-feedback{margin:-12px 0 16px 28px}
.su a{text-underline-offset:2px}
.su a:focus-visible{outline:2px solid var(--su-accent);outline-offset:2px;border-radius:3px}
.su .btn-primary{--bs-btn-bg:var(--su-accent);--bs-btn-border-color:var(--su-accent);--bs-btn-hover-bg:var(--su-accent-dark);--bs-btn-hover-border-color:var(--su-accent-dark);
  --bs-btn-active-bg:var(--su-accent-dark);--bs-btn-active-border-color:var(--su-accent-dark);--bs-btn-focus-box-shadow:none;
  --bs-btn-font-weight:600;--bs-btn-padding-y:11px;--bs-btn-font-size:.92rem;--bs-btn-border-radius:10px;line-height:1.45}
.su .btn-primary:focus-visible{outline:2px solid var(--su-accent);outline-offset:3px}
.su .su-status:not(:empty){margin-top:14px;padding:10px 12px;border-radius:10px;background:#f0fdf4;color:#166534;font-weight:600;font-size:.84rem}
.su .su-terms{font-size:.78rem;color:var(--su-muted);margin:2px 0 16px}
.su .su-foot{margin-top:18px;text-align:center;color:var(--su-muted);font-size:.86rem}
.card.su{display:block} /* plain block flow, so vertical margins match the CSS version */
@media (max-width:420px){.su{padding:26px 20px 22px}}
@media (prefers-reduced-motion:reduce){.su .form-control,.su .input-group,.su .form-check-input,.su .form-floating>label,.su .btn{transition:none}}
JavaScript
// Show/hide password, and inline errors on submit using Bootstrap's .is-invalid. No libraries, no Bootstrap JS.
// Without the script the eye button stays hidden and the browser's own validation still checks the required fields.
document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
  var input = document.getElementById(btn.getAttribute('aria-controls'));
  btn.hidden = false;
  btn.addEventListener('click', function () {
    var show = input.type === 'password';
    input.type = show ? 'text' : 'password';
    btn.setAttribute('aria-pressed', show ? 'true' : 'false');
  });
});
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var status = form.querySelector('[data-status]');
  form.noValidate = true;
  function check(input) {
    var v = input.validity, msg = v.valid ? '' : v.valueMissing ? input.dataset.missing : (input.dataset.bad || input.validationMessage);
    var out = document.getElementById(input.id + '-err');
    input.classList.toggle('is-invalid', !!msg);
    if (msg) input.setAttribute('aria-invalid', 'true'); else input.removeAttribute('aria-invalid');
    if (out) out.textContent = msg;
    return !msg;
  }
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent
    var bad = Array.prototype.filter.call(form.querySelectorAll('[required]'), function (f) { return !check(f); });
    status.textContent = '';
    if (bad.length) { bad[0].focus(); return; }
    status.textContent = 'Account created. We sent a confirmation link to ' + form.elements.email.value + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] items-start gap-[34px]">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6259] uppercase">Classic card</span>
<form class="rounded-[18px] bg-white px-8 pt-[34px] pb-7 text-[.92rem] leading-[1.45] text-su-ink shadow-[0_1px_2px_rgba(28,25,23,.06),0_14px_34px_-14px_rgba(28,25,23,.22)] max-[420px]:px-5 max-[420px]:pt-[26px] max-[420px]:pb-[22px] [--su-accent:#9a3412] [--su-accent-dark:#7c2d12] [--su-ring:rgba(154,52,18,.22)]" action="#" method="post" data-signup aria-labelledby="fn-title">
  <div class="mb-[22px] flex items-center gap-2.5 font-bold"><span class="grid size-8 place-items-center rounded-[9px] bg-(--su-accent) text-white"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z"/><path d="M9 9h6M9 13h4"/></svg></span>Fieldnotes</div>
  <h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.62rem] leading-[1.15] font-normal tracking-[-.01em]" id="fn-title">Start your journal</h2>
  <p class="mt-1.5 mb-5 text-su-muted">Free for your first 100 entries. No card needed.</p>
  <p class="mb-3.5 text-[.8rem] text-su-muted"><span class="font-bold text-(--su-accent)" aria-hidden="true">*</span> Required field</p>
  <div class="mb-4 min-w-0">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="fn-name">Full name <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
    <input class="block w-full rounded-[10px] border border-su-line bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 focus:border-(--su-accent) focus:shadow-[0_0_0_3px_var(--su-ring)] focus:outline-none aria-invalid:border-su-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(185,28,28,.2)] motion-reduce:transition-none" id="fn-name" name="name" type="text" autocomplete="name" required aria-describedby="fn-name-err" data-missing="Enter your name.">
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="fn-name-err"></p>
  </div>
  <div class="mb-4 min-w-0">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="fn-email">Email <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
    <input class="block w-full rounded-[10px] border border-su-line bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 focus:border-(--su-accent) focus:shadow-[0_0_0_3px_var(--su-ring)] focus:outline-none aria-invalid:border-su-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(185,28,28,.2)] motion-reduce:transition-none" id="fn-email" name="email" type="email" autocomplete="email" required aria-describedby="fn-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="fn-email-err"></p>
  </div>
  <div class="mb-4 min-w-0">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="fn-pw">Password <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
    <div class="flex rounded-[10px] border border-su-line bg-white transition-[border-color,box-shadow] duration-150 focus-within:border-(--su-accent) focus-within:shadow-[0_0_0_3px_var(--su-ring)] has-[[aria-invalid]]:border-su-error motion-reduce:transition-none">
      <input class="min-w-0 flex-1 rounded-l-[10px] bg-transparent px-[13px] py-[11px] focus:outline-none" id="fn-pw" name="password" type="password" autocomplete="new-password" minlength="8" required aria-describedby="fn-pw-hint fn-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
      <button class="group grid w-[46px] flex-none cursor-pointer place-items-center rounded-r-[10px] text-su-muted hover:text-su-ink focus-visible:rounded-lg focus-visible:outline-2 focus-visible:-outline-offset-5 focus-visible:outline-(--su-accent)" type="button" aria-controls="fn-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="group-aria-pressed:hidden" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="hidden group-aria-pressed:block" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <p class="mt-1.5 text-[.8rem] text-su-muted" id="fn-pw-hint">At least 8 characters.</p>
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="fn-pw-err"></p>
  </div>
  <div>
    <label class="mt-1 mb-[18px] flex cursor-pointer items-start gap-2.5 text-[.86rem]"><input class="mt-px size-[18px] flex-none cursor-pointer appearance-none rounded-[5px] border-[1.5px] border-su-line bg-white bg-[length:12px_12px] bg-center bg-no-repeat transition-[background-color,border-color,color] duration-150 checked:border-(--su-accent) checked:bg-(--su-accent) checked:su-tick focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent) aria-invalid:border-su-error motion-reduce:transition-none" id="fn-terms" name="terms" type="checkbox" required aria-describedby="fn-terms-err" data-missing="Tick the box to accept the terms."><span>I agree to the <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Terms</a> and <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Privacy Policy</a> <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></span></label>
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!'] -mt-3 mb-4 ml-7" id="fn-terms-err"></p>
  </div>
  <button class="block w-full cursor-pointer rounded-[10px] bg-(--su-accent) px-4 py-3 font-semibold text-white transition-[background-color,border-color,color] duration-150 hover:bg-(--su-accent-dark) focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-(--su-accent) motion-reduce:transition-none" type="submit">Create account</button>
  <p class="text-[.84rem] font-semibold text-[#166534] not-empty:mt-3.5 not-empty:rounded-[10px] not-empty:bg-[#f0fdf4] not-empty:px-3 not-empty:py-2.5" role="status" data-status></p>
  <p class="mt-[18px] text-center text-[.86rem] text-su-muted">Already have an account? <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Log in</a></p>
</form>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6259] uppercase">Floating labels</span>
<form class="rounded-[18px] bg-white px-8 pt-[34px] pb-7 text-[.92rem] leading-[1.45] text-su-ink shadow-[0_1px_2px_rgba(28,25,23,.06),0_14px_34px_-14px_rgba(28,25,23,.22)] max-[420px]:px-5 max-[420px]:pt-[26px] max-[420px]:pb-[22px] [--su-accent:#166534] [--su-accent-dark:#14532d] [--su-ring:rgba(22,101,52,.22)]" action="#" method="post" data-signup aria-labelledby="cb-title">
  <div class="mb-[22px] flex items-center gap-2.5 font-bold"><span class="grid size-8 place-items-center rounded-[9px] bg-(--su-accent) text-white"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16l-1.5 11a2 2 0 0 1-2 1.7h-9a2 2 0 0 1-2-1.7z"/><path d="M9 7a3 3 0 0 1 6 0"/></svg></span>Cobble</div>
  <h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.62rem] leading-[1.15] font-normal tracking-[-.01em]" id="cb-title">One list for the whole house</h2>
  <p class="mt-1.5 mb-5 text-su-muted">Shared grocery lists that update while you shop.</p>
  <p class="mb-3.5 text-[.8rem] text-su-muted"><span class="font-bold text-(--su-accent)" aria-hidden="true">*</span> Required field</p>
  <div class="grid grid-cols-2 gap-x-4 max-[420px]:grid-cols-1">
    <div class="mb-4 min-w-0 relative">
      <input class="peer block w-full rounded-[10px] border border-su-line bg-white px-[13px] pt-[21px] pb-[7px] transition-[border-color,box-shadow] duration-150 focus:border-(--su-accent) focus:shadow-[0_0_0_3px_var(--su-ring)] focus:outline-none aria-invalid:border-su-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(185,28,28,.2)] motion-reduce:transition-none" id="cb-first" name="given-name" type="text" autocomplete="given-name" placeholder=" " required aria-describedby="cb-first-err" data-missing="Enter your first name.">
      <label class="pointer-events-none absolute top-3.5 left-3.5 origin-top-left text-[.92rem] text-su-muted transition-[translate,scale,color] duration-150 peer-focus:-translate-y-2 peer-focus:scale-[.78] peer-focus:font-semibold peer-focus:text-su-ink peer-not-placeholder-shown:-translate-y-2 peer-not-placeholder-shown:scale-[.78] peer-not-placeholder-shown:font-semibold peer-not-placeholder-shown:text-su-ink motion-reduce:transition-none" for="cb-first">First name <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
      <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="cb-first-err"></p>
    </div>
    <div class="mb-4 min-w-0 relative">
      <input class="peer block w-full rounded-[10px] border border-su-line bg-white px-[13px] pt-[21px] pb-[7px] transition-[border-color,box-shadow] duration-150 focus:border-(--su-accent) focus:shadow-[0_0_0_3px_var(--su-ring)] focus:outline-none aria-invalid:border-su-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(185,28,28,.2)] motion-reduce:transition-none" id="cb-last" name="family-name" type="text" autocomplete="family-name" placeholder=" ">
      <label class="pointer-events-none absolute top-3.5 left-3.5 origin-top-left text-[.92rem] text-su-muted transition-[translate,scale,color] duration-150 peer-focus:-translate-y-2 peer-focus:scale-[.78] peer-focus:font-semibold peer-focus:text-su-ink peer-not-placeholder-shown:-translate-y-2 peer-not-placeholder-shown:scale-[.78] peer-not-placeholder-shown:font-semibold peer-not-placeholder-shown:text-su-ink motion-reduce:transition-none" for="cb-last">Last name</label>
    </div>
  </div>
  <div class="mb-4 min-w-0 relative">
    <input class="peer block w-full rounded-[10px] border border-su-line bg-white px-[13px] pt-[21px] pb-[7px] transition-[border-color,box-shadow] duration-150 focus:border-(--su-accent) focus:shadow-[0_0_0_3px_var(--su-ring)] focus:outline-none aria-invalid:border-su-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(185,28,28,.2)] motion-reduce:transition-none" id="cb-email" name="email" type="email" autocomplete="email" placeholder=" " required aria-describedby="cb-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected].">
    <label class="pointer-events-none absolute top-3.5 left-3.5 origin-top-left text-[.92rem] text-su-muted transition-[translate,scale,color] duration-150 peer-focus:-translate-y-2 peer-focus:scale-[.78] peer-focus:font-semibold peer-focus:text-su-ink peer-not-placeholder-shown:-translate-y-2 peer-not-placeholder-shown:scale-[.78] peer-not-placeholder-shown:font-semibold peer-not-placeholder-shown:text-su-ink motion-reduce:transition-none" for="cb-email">Email <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="cb-email-err"></p>
  </div>
  <div class="mb-4 min-w-0">
    <div class="flex rounded-[10px] border border-su-line bg-white transition-[border-color,box-shadow] duration-150 focus-within:border-(--su-accent) focus-within:shadow-[0_0_0_3px_var(--su-ring)] has-[[aria-invalid]]:border-su-error motion-reduce:transition-none relative">
      <input class="peer min-w-0 flex-1 rounded-l-[10px] bg-transparent pt-[21px] pr-1.5 pb-[7px] pl-[13px] focus:outline-none" id="cb-pw" name="password" type="password" autocomplete="new-password" minlength="8" placeholder=" " required aria-describedby="cb-pw-hint cb-pw-err" data-missing="Create a password." data-bad="Use at least 8 characters.">
      <label class="pointer-events-none absolute top-3.5 left-3.5 origin-top-left text-[.92rem] text-su-muted transition-[translate,scale,color] duration-150 peer-focus:-translate-y-2 peer-focus:scale-[.78] peer-focus:font-semibold peer-focus:text-su-ink peer-not-placeholder-shown:-translate-y-2 peer-not-placeholder-shown:scale-[.78] peer-not-placeholder-shown:font-semibold peer-not-placeholder-shown:text-su-ink motion-reduce:transition-none" for="cb-pw">Password <span class="font-bold text-(--su-accent)" aria-hidden="true">*</span></label>
      <button class="group grid w-[46px] flex-none cursor-pointer place-items-center rounded-r-[10px] text-su-muted hover:text-su-ink focus-visible:rounded-lg focus-visible:outline-2 focus-visible:-outline-offset-5 focus-visible:outline-(--su-accent)" type="button" aria-controls="cb-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="group-aria-pressed:hidden" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="hidden group-aria-pressed:block" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <p class="mt-1.5 text-[.8rem] text-su-muted" id="cb-pw-hint">At least 8 characters.</p>
    <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] font-semibold text-su-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-su-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="cb-pw-err"></p>
  </div>
  <label class="mt-1 mb-[18px] flex cursor-pointer items-start gap-2.5 text-[.86rem]"><input class="mt-px size-[18px] flex-none cursor-pointer appearance-none rounded-[5px] border-[1.5px] border-su-line bg-white bg-[length:12px_12px] bg-center bg-no-repeat transition-[background-color,border-color,color] duration-150 checked:border-(--su-accent) checked:bg-(--su-accent) checked:su-tick focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent) aria-invalid:border-su-error motion-reduce:transition-none" id="cb-tips" name="tips" type="checkbox"><span>Send me a weekly meal-plan email (optional)</span></label>
  <p class="mt-0.5 mb-4 text-[.78rem] text-su-muted">By creating an account you agree to the <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Terms</a> and <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Privacy Policy</a>.</p>
  <button class="block w-full cursor-pointer rounded-[10px] bg-(--su-accent) px-4 py-3 font-semibold text-white transition-[background-color,border-color,color] duration-150 hover:bg-(--su-accent-dark) focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-(--su-accent) motion-reduce:transition-none" type="submit">Create free account</button>
  <p class="text-[.84rem] font-semibold text-[#166534] not-empty:mt-3.5 not-empty:rounded-[10px] not-empty:bg-[#f0fdf4] not-empty:px-3 not-empty:py-2.5" role="status" data-status></p>
  <p class="mt-[18px] text-center text-[.86rem] text-su-muted">Already have an account? <a class="text-(--su-accent) underline underline-offset-2 hover:text-(--su-accent-dark) focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--su-accent)" href="#">Log in</a></p>
</form>
</div>
</div>
CSS
@theme {
  --color-su-ink: #1c1917;
  --color-su-muted: #57534e;
  --color-su-line: #8a817a;
  --color-su-error: #b91c1c;
}
/* white tick for the checked checkbox */
@utility su-tick {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
JavaScript
// Show/hide password, and inline errors on submit. No libraries. Without the script the eye button
// stays hidden and the browser's own validation still checks the required fields.
document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
  var input = document.getElementById(btn.getAttribute('aria-controls'));
  btn.hidden = false;
  btn.addEventListener('click', function () {
    var show = input.type === 'password';
    input.type = show ? 'text' : 'password';
    btn.setAttribute('aria-pressed', show ? 'true' : 'false');
  });
});
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var status = form.querySelector('[data-status]');
  form.noValidate = true;
  function check(input) {
    var v = input.validity, msg = v.valid ? '' : v.valueMissing ? input.dataset.missing : (input.dataset.bad || input.validationMessage);
    var out = document.getElementById(input.id + '-err');
    input.classList.toggle('is-invalid', !!msg);
    if (msg) input.setAttribute('aria-invalid', 'true'); else input.removeAttribute('aria-invalid');
    if (out) out.textContent = msg;
    return !msg;
  }
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent
    var bad = Array.prototype.filter.call(form.querySelectorAll('[required]'), function (f) { return !check(f); });
    status.textContent = '';
    if (bad.length) { bad[0].focus(); return; }
    status.textContent = 'Account created. We sent a confirmation link to ' + form.elements.email.value + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});

About this set

Two single-card sign-up forms that cover the account creation screen most products need. Classic card is a journaling app sign-up with stacked labels above each field: full name, email, a password with a show and hide button, and a required terms checkbox with links to the terms and privacy policy. Floating labels is a grocery list app sign-up where each label starts inside its field and rises when the field has focus or a value; first and last name share a row that stacks on narrow screens, and an optional meal-plan email checkbox sits above a line of terms text. Use the classic card when clarity matters most and the floating version where vertical space is tight. Both forms keep the browser’s own validation when JavaScript is off. A short script switches it to inline messages: on submit every required field is checked, each failing field gets aria-invalid, a red border and a message with an icon written into its linked error element, and focus moves to the first problem. Flagged fields re-check as you type, and a valid submit shows a confirmation in a status region. The wording of each message lives in data attributes on its input.

What’s included

  • Two layouts: stacked labels and floating labels
  • Show and hide password button with aria-pressed, hidden until the script runs
  • Inline error messages from data-missing and data-bad attributes, focus moved to the first error
  • Required fields marked with an asterisk and a written key, not colour alone
  • Bootstrap version uses .form-control, .form-floating, .input-group, .form-check and .is-invalid

Accessibility

Every input has a visible label and an autocomplete value (name, given-name, family-name, email, new-password). Error messages are linked through aria-describedby and the input gets aria-invalid="true" while a message shows; focus moves to the first invalid field and a valid submit is announced through role="status". The password toggle is a real button named Show password whose aria-pressed state changes, and every control has a visible focus ring. Transitions switch off under prefers-reduced-motion.

Customise it

Change --su-accent, --su-accent-dark and --su-ring on .su for the brand colour (.su--green shows a second palette), and --su-line for field borders. In Bootstrap the same variables sit on the .card along with --bs-border-radius; in Tailwind set the [--su-accent:…] properties on the form and edit the su-* theme colours.