beta

Auth — <mv-auth>

Four-step authentication block (log in, sign up, forgot password, OTP code): split layout with visual and testimonial, social buttons, password reveal, strength meter, timed code resend, native validation and async submission.

CategoryBlocks
TypeWeb Component (<mv-auth>)
Statusbeta
Also installsbutton, checkbox, field, input, otp, separator
Keywordsauth, login, signup, sign-in, forgot-password, otp, 2fa, form

When to use

Avoid when

Install

node scripts/add.mjs auth --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["auth"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, components/auth/auth.js, components/auth/auth.css, components/button/button.css, components/checkbox/checkbox.css, components/field/field.css, components/input/input.css, components/otp/otp.js, components/otp/otp.css, components/separator/separator.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="auth-demo" style="display:grid;grid-template-columns:minmax(0,1fr);gap:.75rem;padding:1rem">
  <div class="mv-button-group" role="group" aria-label="Displayed step" style="justify-self:center;max-width:100%;overflow-x:auto">
    <button class="mv-button" data-variant="outline" data-size="sm" data-demo-view="login" aria-pressed="true">Log in</button>
    <button class="mv-button" data-variant="outline" data-size="sm" data-demo-view="signup" aria-pressed="false">Sign up</button>
    <button class="mv-button" data-variant="outline" data-size="sm" data-demo-view="forgot" aria-pressed="false">Forgot password</button>
    <button class="mv-button" data-variant="outline" data-size="sm" data-demo-view="otp" aria-pressed="false">OTP code</button>
  </div>

  <mv-auth data-variant="login" style="min-height:680px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);overflow:hidden">
    <div class="mv-auth-grid">
      <aside class="mv-auth-visual" aria-label="Customer testimonial">
        <a class="mv-auth-brand" href="#auth-demo"><span class="mv-auth-brand-mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19V5l8 7 8-7v14"/></svg></span>Nova Works</a>

        <div class="mv-auth-showcase" aria-hidden="true">
          <div class="mv-auth-glass">
            <small>Time saved per week</small>
            <strong>6h 40m</strong>
            <svg viewBox="0 0 200 48" preserveAspectRatio="none" fill="none"><defs><linearGradient id="auth-demo-g" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="currentColor" stop-opacity=".35"/><stop offset="1" stop-color="currentColor" stop-opacity="0"/></linearGradient></defs><path d="M0 40 C20 38 30 30 50 32 S80 22 100 24 S130 12 150 14 S185 4 200 6 V48 H0Z" fill="url(#auth-demo-g)"/><path d="M0 40 C20 38 30 30 50 32 S80 22 100 24 S130 12 150 14 S185 4 200 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
          </div>
          <div class="mv-auth-glass" data-offset>
            <small>Active teams</small>
            <strong>2,400+</strong>
          </div>
        </div>

        <blockquote class="mv-auth-quote">
          <p>“Since we moved quotes, scheduling and invoicing into Nova Works, we’ve stopped chasing files. The whole team can breathe again.”</p>
          <footer>
            <span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:40" aria-hidden="true"><span class="mv-avatar-fallback">SN</span></span>
            <span><strong>Sofia Nguyen</strong><small>Director, Kōan Studio</small></span>
          </footer>
        </blockquote>
      </aside>

      <div class="mv-auth-panel">
        <div class="mv-auth-top">
          <a class="mv-auth-brand" href="#auth-demo"><span class="mv-auth-brand-mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19V5l8 7 8-7v14"/></svg></span>Nova Works</a>
        </div>

        <main class="mv-auth-body">
          <!-- Login -->
          <section class="mv-auth-view" data-view="login">
            <header class="mv-auth-head">
              <h1>Welcome back</h1>
              <p>Log in to pick up where you left off.</p>
            </header>
            <div class="mv-auth-social">
              <button class="mv-button" data-variant="outline" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M21.8 10.2H12v3.9h5.6c-.5 2.6-2.8 4.1-5.6 4.1a6.2 6.2 0 0 1 0-12.4c1.5 0 2.9.5 4 1.5l2.9-2.9A10.2 10.2 0 0 0 12 1.8a10.2 10.2 0 1 0 0 20.4c5.9 0 9.9-4.1 9.9-10 0-.7-.1-1.3-.1-2z"/></svg>Google</button>
              <button class="mv-button" data-variant="outline" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>GitHub</button>
              <button class="mv-button" data-variant="outline" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M16.37 12.54c-.02-2.18 1.78-3.23 1.86-3.28-1.02-1.49-2.6-1.69-3.16-1.71-1.34-.14-2.62.79-3.3.79-.68 0-1.73-.77-2.84-.75-1.46.02-2.81.85-3.56 2.16-1.52 2.63-.39 6.53 1.09 8.67.72 1.05 1.58 2.22 2.71 2.18 1.09-.04 1.5-.7 2.81-.7 1.31 0 1.68.7 2.83.68 1.17-.02 1.91-1.07 2.62-2.12.83-1.21 1.17-2.39 1.19-2.45-.03-.01-2.28-.87-2.3-3.47zM14.21 6.14c.6-.73 1-1.73.89-2.74-.86.03-1.91.58-2.52 1.3-.55.64-1.04 1.67-.91 2.65.96.07 1.94-.49 2.54-1.21z"/></svg>Apple</button>
            </div>
            <div class="mv-separator" style="font-size:.75rem">or continue with email</div>
            <form class="mv-auth-form" novalidate>
              <div class="mv-field">
                <label class="mv-label" for="auth-demo-email">Email address</label>
                <input class="mv-input" id="auth-demo-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required>
              </div>
              <div class="mv-field">
                <div class="mv-field-header">
                  <label class="mv-label" for="auth-demo-pass">Password</label>
                  <a href="#auth-demo" data-auth-view="forgot">Forgot password?</a>
                </div>
                <div class="mv-input-group">
                  <input class="mv-input" id="auth-demo-pass" name="password" type="password" autocomplete="current-password" required minlength="8">
                  <button class="mv-input-group-addon mv-button" data-variant="ghost" data-size="icon" data-auth-reveal aria-label="Show password" style="margin-inline-end:.125rem;height:1.875rem;width:1.875rem">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 4.2A9.8 9.8 0 0 1 12 4c6.5 0 10 8 10 8a17 17 0 0 1-2.2 3.2M6.6 6.6A17.4 17.4 0 0 0 2 12s3.5 8 10 8a9.7 9.7 0 0 0 5.4-1.6M14.1 14.1a3 3 0 1 1-4.2-4.2M2 2l20 20"/></svg>
                  </button>
                </div>
              </div>
              <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="remember" checked> Keep me signed in for 30 days</label>
              <p class="mv-auth-status" role="status" hidden></p>
              <button class="mv-button" type="submit">Log in</button>
            </form>
            <p class="mv-auth-alt">Don’t have an account? <a href="#auth-demo" data-auth-view="signup">Sign up</a></p>
          </section>

          <!-- Signup -->
          <section class="mv-auth-view" data-view="signup" hidden>
            <header class="mv-auth-head">
              <h1>Create your workspace</h1>
              <p>14-day free trial, no credit card required.</p>
            </header>
            <div class="mv-auth-social">
              <button class="mv-button" data-variant="outline" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M21.8 10.2H12v3.9h5.6c-.5 2.6-2.8 4.1-5.6 4.1a6.2 6.2 0 0 1 0-12.4c1.5 0 2.9.5 4 1.5l2.9-2.9A10.2 10.2 0 0 0 12 1.8a10.2 10.2 0 1 0 0 20.4c5.9 0 9.9-4.1 9.9-10 0-.7-.1-1.3-.1-2z"/></svg>Google</button>
              <button class="mv-button" data-variant="outline" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>GitHub</button>
            </div>
            <div class="mv-separator" style="font-size:.75rem">or</div>
            <form class="mv-auth-form" novalidate>
              <div class="mv-field-row">
                <div class="mv-field">
                  <label class="mv-label" for="auth-demo-first">First name</label>
                  <input class="mv-input" id="auth-demo-first" name="firstName" autocomplete="given-name" required placeholder="Maya">
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="auth-demo-last">Last name</label>
                  <input class="mv-input" id="auth-demo-last" name="lastName" autocomplete="family-name" required placeholder="Rodriguez">
                </div>
              </div>
              <div class="mv-field">
                <label class="mv-label" for="auth-demo-email2">Work email</label>
                <input class="mv-input" id="auth-demo-email2" name="email" type="email" autocomplete="email" required placeholder="[email protected]">
              </div>
              <div class="mv-field">
                <label class="mv-label" for="auth-demo-newpass">Password</label>
                <div class="mv-input-group">
                  <input class="mv-input" id="auth-demo-newpass" name="password" type="password" autocomplete="new-password" required minlength="8" data-auth-strength aria-describedby="auth-demo-strength">
                  <button class="mv-input-group-addon mv-button" data-variant="ghost" data-size="icon" data-auth-reveal aria-label="Show password" style="margin-inline-end:.125rem;height:1.875rem;width:1.875rem">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 4.2A9.8 9.8 0 0 1 12 4c6.5 0 10 8 10 8a17 17 0 0 1-2.2 3.2M6.6 6.6A17.4 17.4 0 0 0 2 12s3.5 8 10 8a9.7 9.7 0 0 0 5.4-1.6M14.1 14.1a3 3 0 1 1-4.2-4.2M2 2l20 20"/></svg>
                  </button>
                </div>
                <div class="mv-auth-strength" id="auth-demo-strength"><span></span><span></span><span></span><span></span><span class="mv-auth-strength-label" aria-live="polite">At least 8 characters, with an uppercase letter and a number.</span></div>
              </div>
              <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="terms" required> <span>I agree to the <a href="#auth-demo">Terms of Service</a></span></label>
              <p class="mv-auth-status" role="status" hidden></p>
              <button class="mv-button" type="submit">Create account</button>
            </form>
            <p class="mv-auth-alt">Already have an account? <a href="#auth-demo" data-auth-view="login">Log in</a></p>
          </section>

          <!-- Forgot password -->
          <section class="mv-auth-view" data-view="forgot" hidden>
            <header class="mv-auth-head">
              <span class="mv-auth-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
              <h1>Forgot your password?</h1>
              <p>Enter your email and we’ll send you a reset link, valid for 30 minutes.</p>
            </header>
            <form class="mv-auth-form" novalidate>
              <div class="mv-field">
                <label class="mv-label" for="auth-demo-reset">Email address</label>
                <input class="mv-input" id="auth-demo-reset" name="email" type="email" autocomplete="email" required placeholder="[email protected]">
              </div>
              <p class="mv-auth-status" role="status" hidden></p>
              <button class="mv-button" type="submit">Send reset link</button>
            </form>
            <p class="mv-auth-alt"><a href="#auth-demo" data-auth-view="login">← Back to log in</a></p>
          </section>

          <!-- OTP -->
          <section class="mv-auth-view" data-view="otp" hidden>
            <header class="mv-auth-head">
              <span class="mv-auth-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></svg></span>
              <h1>Check your inbox</h1>
              <p>Enter the 6-digit code we sent to <strong>[email protected]</strong>.</p>
            </header>
            <form class="mv-auth-form" novalidate>
              <mv-otp name="code" length="6" separator required label="Verification code" pattern="numeric"></mv-otp>
              <p class="mv-auth-status" role="status" hidden></p>
              <button class="mv-button" type="submit">Verify</button>
            </form>
            <p class="mv-auth-alt">Didn’t get it? <button type="button" class="mv-auth-link" data-auth-resend data-seconds="30">Resend code</button></p>
          </section>
        </main>

        <p class="mv-auth-legal">By continuing, you agree to our <a href="#auth-demo">Terms</a> and <a href="#auth-demo">Privacy Policy</a>.</p>
      </div>
    </div>
  </mv-auth>
</div>
<script type="module">
  const root = document.getElementById("auth-demo");
  const auth = root?.querySelector("mv-auth");
  const switches = root?.querySelectorAll("[data-demo-view]") ?? [];
  for (const b of switches) b.addEventListener("click", () => auth.show(b.dataset.demoView, { focus: false }));
  auth?.addEventListener("mv-change", (e) => { for (const b of switches) b.setAttribute("aria-pressed", String(b.dataset.demoView === e.detail.view)); });
  // Simulated backend.
  auth?.addEventListener("mv-submit", (e) => {
    e.preventDefault();
    const { view, data } = e.detail;
    setTimeout(() => {
      if (view === "login") auth.done({ message: "Logged in. Redirecting to your dashboard…" });
      else if (view === "signup") auth.done({ next: "otp" });
      else if (view === "forgot") auth.done({ message: `Link sent to ${data.email}. Be sure to check your spam folder.` });
      else if (view === "otp") data.code === "424242" ? auth.done({ message: "Email verified. Welcome aboard!" }) : auth.fail("Incorrect code. For this demo, enter 424242.");
    }, 900);
  });
</script>

API

Attributes

NameTypeDefaultDescription
data-variantlogin | signup | forgot | otploginDisplayed step (matching .mv-auth-view[data-view] section).
data-layoutsplit | cardsplitsplit: visual on the left when the container is wider than 52rem; card: centered card.

Properties

NameTypeDescription
viewstringCurrent step (read/write).

Methods

NameDescription
show(view, { focus })Switches step (fade); focus puts the cursor in the first field.
done({ next?, message? })Completes the pending submission: announces a success message and/or goes to the next step.
fail(message)Failure: announces an error message; an OTP code is cleared and shaken.
passwordScore(value)Export: password strength score 0–4.

Events

NameDescription
mv-submitValid form submitted (or OTP code complete). detail: { view, data, form }. preventDefault() = async handling, then done() / fail(); otherwise resolved immediately.
mv-changeStep changed. detail: { view }.
mv-resendCode resend requested. detail: { view }.

Content structure

NameDescription
.mv-auth-grid > .mv-auth-visual + .mv-auth-panelVisual (aside) and form panel.
.mv-auth-view[data-view]One section per step, with a heading (h1/h2 → aria-labelledby).
[data-auth-view="…"]Link/button that switches step.
[data-auth-reveal]Show/hide password button (inside the .mv-input-group, or value = selector); 2 svgs: eye / eye-off.
input[data-auth-strength] + .mv-auth-strength4-segment meter + .mv-auth-strength-label (aria-live).
[data-auth-resend][data-seconds]Resend button disabled with a countdown.
.mv-auth-statusResult message (role=status), tinted success / error.

CSS classes

NameDescription
mv-auth-brand / -brand-mark / -showcase / -glass / -quoteBrand, decorative glass cards, testimonial.
mv-auth-top / -body / -head / -icon / -social / -form / -alt / -legal / -linkPanel parts.

CSS variables

NameDescription
--mv-auth-visual-a / -b / -bgVisual colors (derived from the accent hue).

Accessibility

Each step is a section named by its heading; focus moves to the first field when the user switches steps. Native validation (reportValidity), correct autocomplete values (email, current-password, new-password, one-time-code via mv-otp). Password reveal button with aria-pressed and an updated label. Strength and results announced (aria-live / role=status). Busy button: aria-busy. Visual: named aside, decorative cards aria-hidden; animation disabled with reduced motion.