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.
Category Blocks Type Web Component (<mv-auth>) Status beta Also installs button , checkbox , field , input , otp , separator Keywords auth, 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
Get Auth. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
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
Name Type Default Description data-variantlogin | signup | forgot | otp loginDisplayed step (matching .mv-auth-view[data-view] section). data-layoutsplit | card splitsplit: visual on the left when the container is wider than 52rem; card: centered card.
Properties
Name Type Description viewstring Current step (read/write).
Methods
Name Description 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
Name Description 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
Name Description .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
Name Description 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
Name Description --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.