Auth <mv-auth>

Bloc d’authentification en quatre étapes (connexion, inscription, mot de passe oublié, code OTP) : mise en page scindée avec visuel et témoignage, boutons de connexion sociale, affichage du mot de passe, jauge de robustesse, renvoi du code minuté, validation native et soumission asynchrone.

CatégorieBlocs
TypeWeb Component (<mv-auth>)
Statutstable
Installe aussibutton, checkbox, field, input, otp, separator
Keywordsauth, login, signup, sign-in, forgot-password, otp, 2fa, form

When to use

  • An app needs log in, sign up, forgot password and OTP verification screens with one consistent look
  • A split sign-in page with a visual and testimonial, or a compact centered card, fits the brand
  • Sign-up must show password strength and a timed code resend while the server call is pending

Avoid when

  • Only a one-time code input is needed inside an existing form → use OTP instead
  • The form edits an existing account's profile or security preferences → use Settings instead

Installation

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

Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["auth"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, components/field/field.css, components/input/input.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, components/otp/otp.js, components/otp/otp.css, components/separator/separator.css, core/focus.js, components/auth/auth.js, components/auth/auth.css.

Utilisation

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :

<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" aria-label="Log in with Google"><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" aria-label="Log in with GitHub"><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" aria-label="Log in with Apple"><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="username" 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>
                  <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" aria-label="Sign up with Google"><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" aria-label="Sign up with GitHub"><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="username" 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"><span aria-hidden="true">← </span>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

NameTypePar défautDescription
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).
stringsobjectDefault texts, partial override: show, strength ("Strength: {level}"), tooShort, weak, fair, good, strong, resendIn ("{label} ({time})", time formatted with Intl), resent, error.

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; on a step change focus moves to the first field and the heading is announced. Native validation (reportValidity) with aria-invalid on the fields to fix; a fail() message is linked to the form's fields (aria-describedby). Autocomplete values: username, current-password, new-password, email, one-time-code (via mv-otp). Password reveal button keeps its name, aria-pressed carries the state. Strength announced only when the level changes; results announced (role=status). Resend: the countdown is not a live region, focus goes to the code field and "New code sent" is announced. Social buttons named by their action ("Log in with Google"). Busy button: aria-busy. Visual: named aside, decorative cards aria-hidden, hidden on narrow screens; animation disabled with reduced motion.