More Layouts Snippets
Mobile Login Screen — Free App Sign In UI Snippet
Mobile Login Screen · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Mobile Login Screen — App Sign-In UI with Validation

A mobile login screen is the sign-in flow of an app — branding, email and password fields, a forgot-password link, a primary submit, and social options. This snippet is a complete, working one inside a CSS phone frame, with real inline validation, a password reveal toggle, and a loading submit state, built in HTML, CSS, and vanilla JavaScript with no dependency. It's a ready starting point for any app's auth screen.
Inline validation on submit
Submitting runs lightweight validation: the email is checked against a pragmatic regex (/^[^\s@]+@[^\s@]+\.[^\s@]+$/ — non-space, an @, more non-space, a dot, a TLD), and the password for a minimum length. A failing field gets a .bad class that turns its border red and shows a specific message ("Email is required" vs "Enter a valid email address"), and focus jumps to the first problem. The error clears as soon as the user edits the field, so the form guides rather than nags.
Password show/hide
The reveal button swaps the input's type between password and text and updates its aria-label and color accordingly — the standard, accessible pattern for letting users verify what they typed without exposing it permanently. It's a single toggle with no second input.
A submit that shows progress
On a valid submit, the button enters a .loading state (disabled via pointer-events:none, lightened, label changed to "Signing in…") and, after a simulated round-trip, confirms success. In production you'd await your auth call here; the choke point is one handler, so wiring a real request is a one-line change.
The screen layout
Inside the phone frame, the screen is a scrollable column: brand row, heading, the form, an "or continue with" divider built from a flex row with ::before/::after rules, and social buttons. The status bar with a CSS battery completes the device illusion. Everything is real, focusable, keyboard-navigable DOM.
Reusing it
Drop it into a phone mockup for a marketing shot, or lift the form out of the frame to use as a responsive web login. Replace the validation and submit handler with your auth provider, keep the structure, and you have a production-ready sign-in screen.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA mobile login screen renders inside a phone frame.
- 2Submit emptyThe email field turns red with a required message.
- 3Enter a bad emailValidation shows a specific invalid-email error.
- 4Toggle the passwordThe eye button reveals and hides what you typed.
- 5Submit valid dataThe button shows a loading state then confirms success.
- 6Wire your authReplace the simulated round-trip with your provider.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
On submit, the email is tested against the regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ — it requires non-space characters, an @, more non-space, a dot, and a domain ending. If it fails, the field gets a red border and a specific message: "Email is required" when empty, or "Enter a valid email address" when malformed. The error clears as soon as you start editing.
The eye button flips the password input's type attribute between password and text, which reveals or masks the characters, and updates the button's aria-label and color so screen readers and sighted users both know the current state. It's a single input with a toggle, not two overlapping fields.
When validation passes, the submit button gets a loading class that lightens it, disables interaction with pointer-events:none, and changes the label to "Signing in…". The snippet simulates a round-trip with a timeout then shows success; in a real app you'd await your authentication call in that same handler.
Yes. The phone frame is just a wrapper; the form and its validation are independent. Lift the form markup out and it works as a responsive web login. The frame is useful for presentations and app mockups, but nothing in the logic depends on it.
Bind the email and password to state, run the validation in your submit handler, and toggle error classes from validation results. The password reveal flips an input type bound to a showPassword flag. Replace the timeout with your auth API call and manage the loading flag in state. Tailwind styles the fields, divider, and buttons with utilities.
Mobile Login Screen — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mobile Login Screen</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.mls-phone{width:288px;height:600px;background:#0b1220;border-radius:46px;padding:12px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 0 2px #1e293b}
.mls-screen{width:100%;height:100%;border-radius:34px;overflow:hidden;background:#fff;color:#0f172a;display:flex;flex-direction:column}
.mls-status{display:flex;justify-content:space-between;align-items:center;padding:13px 24px 0;font-size:13px;font-weight:700}
.mls-batt{width:22px;height:11px;border:1.4px solid #0f172a;border-radius:3px;position:relative;display:inline-block}
.mls-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0}
.mls-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px}
.mls-body{flex:1;overflow-y:auto;padding:26px 24px 20px}
.mls-brand{display:flex;align-items:center;gap:9px;margin-bottom:30px}
.mls-logo{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#6366f1,#22d3ee)}
.mls-brand span{font-size:15px;font-weight:800}
.mls-title{font-size:25px;font-weight:800}
.mls-sub{font-size:13px;color:#94a3b8;margin:5px 0 22px}
.mls-field{display:block;margin-bottom:15px}
.mls-field>span{font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px}
.mls-field input{width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s}
.mls-field input:focus{border-color:#6366f1}
.mls-field.bad input{border-color:#ef4444}
.mls-err{display:block;font-size:11px;color:#ef4444;font-style:normal;margin-top:5px;min-height:13px}
.mls-pw{position:relative}
.mls-eye{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#94a3b8;cursor:pointer;padding:5px}
.mls-eye:hover{color:#475569}
.mls-forgot{display:block;text-align:right;font-size:12px;font-weight:600;color:#6366f1;text-decoration:none;margin-bottom:18px}
.mls-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:12px;padding:14px;font-size:14.5px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
.mls-submit:hover{background:#4f46e5}
.mls-submit.loading{background:#818cf8;pointer-events:none}
.mls-or{display:flex;align-items:center;gap:10px;margin:20px 0;color:#94a3b8;font-size:11.5px}
.mls-or::before,.mls-or::after{content:'';flex:1;height:1px;background:#e2e8f0}
.mls-social{display:flex;gap:10px}
.mls-social button{flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}
.mls-social button:hover{background:#f8fafc}
.mls-foot{text-align:center;font-size:12.5px;color:#64748b;margin-top:20px}
.mls-foot a{color:#6366f1;font-weight:700;text-decoration:none}
</style>
</head>
<body>
<div class="mls-phone">
<div class="mls-screen">
<div class="mls-status"><span>9:41</span><span class="mls-batt"><i></i></span></div>
<div class="mls-body">
<div class="mls-brand"><div class="mls-logo"></div><span>Northwind</span></div>
<h1 class="mls-title">Welcome back</h1>
<p class="mls-sub">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label class="mls-field">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off">
<em class="mls-err" id="mlsEmailErr"></em>
</label>
<label class="mls-field">
<span>Password</span>
<div class="mls-pw">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off">
<button type="button" class="mls-eye" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a class="mls-forgot" href="#">Forgot password?</a>
<button type="submit" class="mls-submit" id="mlsSubmit">Sign in</button>
</form>
<div class="mls-or"><span>or continue with</span></div>
<div class="mls-social"><button type="button">Google</button><button type="button">Apple</button></div>
<p class="mls-foot">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
<script>
var form = document.getElementById('mlsForm');
var email = document.getElementById('mlsEmail');
var emailErr = document.getElementById('mlsEmailErr');
var pw = document.getElementById('mlsPw');
var eye = document.getElementById('mlsEye');
var submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mobile Login Screen</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.mls-batt::after {
content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0
}
.mls-batt i {
position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px
}
.mls-brand span {
font-size:15px;font-weight:800
}
.mls-field>span {
font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px
}
.mls-field input {
width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s
}
.mls-field input:focus {
border-color:#6366f1
}
.mls-field.bad input {
border-color:#ef4444
}
.mls-submit.loading {
background:#818cf8;pointer-events:none
}
.mls-or::before,.mls-or::after {
content:'';flex:1;height:1px;background:#e2e8f0
}
.mls-social button {
flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit
}
.mls-social button:hover {
background:#f8fafc
}
.mls-foot a {
color:#6366f1;font-weight:700;text-decoration:none
}
</style>
</head>
<body>
<div class="w-72 h-[600px] bg-[#0b1220] rounded-[46px] p-3 shadow-[0_30px_60px_-20px_rgba(0,0,0,.6),inset_0_0_0_2px_#1e293b]">
<div class="w-full h-full rounded-[34px] overflow-hidden bg-[#fff] text-[#0f172a] flex flex-col">
<div class="flex justify-between items-center pt-[13px] px-6 pb-0 text-[13px] font-bold"><span>9:41</span><span class="mls-batt w-[22px] h-[11px] border rounded-[3px] relative inline-block"><i></i></span></div>
<div class="flex-1 overflow-y-auto pt-[26px] px-6 pb-5">
<div class="mls-brand flex items-center gap-[9px] mb-[30px]"><div class="w-[30px] h-[30px] rounded-[9px] [background:linear-gradient(135deg,#6366f1,#22d3ee)]"></div><span>Northwind</span></div>
<h1 class="text-[25px] font-extrabold">Welcome back</h1>
<p class="text-[13px] text-[#94a3b8] mt-[5px] mx-0 mb-[22px]">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label class="mls-field block mb-[15px]">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off">
<em class="block text-[11px] text-[#ef4444] not-italic mt-[5px] min-h-[13px]" id="mlsEmailErr"></em>
</label>
<label class="mls-field block mb-[15px]">
<span>Password</span>
<div class="relative">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off">
<button type="button" class="absolute right-2 top-1/2 [transform:translateY(-50%)] bg-transparent border-0 text-[#94a3b8] cursor-pointer p-[5px] hover:text-[#475569]" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a class="block text-right text-xs font-semibold text-[#6366f1] no-underline mb-[18px]" href="#">Forgot password?</a>
<button type="submit" class="mls-submit w-full bg-[#6366f1] text-[#fff] border-0 rounded-xl p-3.5 text-[14.5px] font-bold cursor-pointer font-[inherit] [transition:background_.15s] hover:bg-[#4f46e5]" id="mlsSubmit">Sign in</button>
</form>
<div class="mls-or flex items-center gap-2.5 my-5 mx-0 text-[#94a3b8] text-[11.5px]"><span>or continue with</span></div>
<div class="mls-social flex gap-2.5"><button type="button">Google</button><button type="button">Apple</button></div>
<p class="mls-foot text-center text-[12.5px] text-[#64748b] mt-5">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
<script>
var form = document.getElementById('mlsForm');
var email = document.getElementById('mlsEmail');
var emailErr = document.getElementById('mlsEmailErr');
var pw = document.getElementById('mlsPw');
var eye = document.getElementById('mlsEye');
var submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to MobileLoginScreen.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.mls-phone{width:288px;height:600px;background:#0b1220;border-radius:46px;padding:12px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 0 2px #1e293b}
.mls-screen{width:100%;height:100%;border-radius:34px;overflow:hidden;background:#fff;color:#0f172a;display:flex;flex-direction:column}
.mls-status{display:flex;justify-content:space-between;align-items:center;padding:13px 24px 0;font-size:13px;font-weight:700}
.mls-batt{width:22px;height:11px;border:1.4px solid #0f172a;border-radius:3px;position:relative;display:inline-block}
.mls-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0}
.mls-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px}
.mls-body{flex:1;overflow-y:auto;padding:26px 24px 20px}
.mls-brand{display:flex;align-items:center;gap:9px;margin-bottom:30px}
.mls-logo{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#6366f1,#22d3ee)}
.mls-brand span{font-size:15px;font-weight:800}
.mls-title{font-size:25px;font-weight:800}
.mls-sub{font-size:13px;color:#94a3b8;margin:5px 0 22px}
.mls-field{display:block;margin-bottom:15px}
.mls-field>span{font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px}
.mls-field input{width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s}
.mls-field input:focus{border-color:#6366f1}
.mls-field.bad input{border-color:#ef4444}
.mls-err{display:block;font-size:11px;color:#ef4444;font-style:normal;margin-top:5px;min-height:13px}
.mls-pw{position:relative}
.mls-eye{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#94a3b8;cursor:pointer;padding:5px}
.mls-eye:hover{color:#475569}
.mls-forgot{display:block;text-align:right;font-size:12px;font-weight:600;color:#6366f1;text-decoration:none;margin-bottom:18px}
.mls-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:12px;padding:14px;font-size:14.5px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
.mls-submit:hover{background:#4f46e5}
.mls-submit.loading{background:#818cf8;pointer-events:none}
.mls-or{display:flex;align-items:center;gap:10px;margin:20px 0;color:#94a3b8;font-size:11.5px}
.mls-or::before,.mls-or::after{content:'';flex:1;height:1px;background:#e2e8f0}
.mls-social{display:flex;gap:10px}
.mls-social button{flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}
.mls-social button:hover{background:#f8fafc}
.mls-foot{text-align:center;font-size:12.5px;color:#64748b;margin-top:20px}
.mls-foot a{color:#6366f1;font-weight:700;text-decoration:none}
`;
export default function MobileLoginScreen() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var form = document.getElementById('mlsForm');
var email = document.getElementById('mlsEmail');
var emailErr = document.getElementById('mlsEmailErr');
var pw = document.getElementById('mlsPw');
var eye = document.getElementById('mlsEye');
var submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="mls-phone">
<div className="mls-screen">
<div className="mls-status"><span>9:41</span><span className="mls-batt"><i></i></span></div>
<div className="mls-body">
<div className="mls-brand"><div className="mls-logo"></div><span>Northwind</span></div>
<h1 className="mls-title">Welcome back</h1>
<p className="mls-sub">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label className="mls-field">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off" />
<em className="mls-err" id="mlsEmailErr"></em>
</label>
<label className="mls-field">
<span>Password</span>
<div className="mls-pw">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off" />
<button type="button" className="mls-eye" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" strokeWidth="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a className="mls-forgot" href="#">Forgot password?</a>
<button type="submit" className="mls-submit" id="mlsSubmit">Sign in</button>
</form>
<div className="mls-or"><span>or continue with</span></div>
<div className="mls-social"><button type="button">Google</button><button type="button">Apple</button></div>
<p className="mls-foot">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function MobileLoginScreen() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var form = document.getElementById('mlsForm');
var email = document.getElementById('mlsEmail');
var emailErr = document.getElementById('mlsEmailErr');
var pw = document.getElementById('mlsPw');
var eye = document.getElementById('mlsEye');
var submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.mls-batt::after {
content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0
}
.mls-batt i {
position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px
}
.mls-brand span {
font-size:15px;font-weight:800
}
.mls-field>span {
font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px
}
.mls-field input {
width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s
}
.mls-field input:focus {
border-color:#6366f1
}
.mls-field.bad input {
border-color:#ef4444
}
.mls-submit.loading {
background:#818cf8;pointer-events:none
}
.mls-or::before,.mls-or::after {
content:'';flex:1;height:1px;background:#e2e8f0
}
.mls-social button {
flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit
}
.mls-social button:hover {
background:#f8fafc
}
.mls-foot a {
color:#6366f1;font-weight:700;text-decoration:none
}
`}</style>
<div className="w-72 h-[600px] bg-[#0b1220] rounded-[46px] p-3 shadow-[0_30px_60px_-20px_rgba(0,0,0,.6),inset_0_0_0_2px_#1e293b]">
<div className="w-full h-full rounded-[34px] overflow-hidden bg-[#fff] text-[#0f172a] flex flex-col">
<div className="flex justify-between items-center pt-[13px] px-6 pb-0 text-[13px] font-bold"><span>9:41</span><span className="mls-batt w-[22px] h-[11px] border rounded-[3px] relative inline-block"><i></i></span></div>
<div className="flex-1 overflow-y-auto pt-[26px] px-6 pb-5">
<div className="mls-brand flex items-center gap-[9px] mb-[30px]"><div className="w-[30px] h-[30px] rounded-[9px] [background:linear-gradient(135deg,#6366f1,#22d3ee)]"></div><span>Northwind</span></div>
<h1 className="text-[25px] font-extrabold">Welcome back</h1>
<p className="text-[13px] text-[#94a3b8] mt-[5px] mx-0 mb-[22px]">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label className="mls-field block mb-[15px]">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off" />
<em className="block text-[11px] text-[#ef4444] not-italic mt-[5px] min-h-[13px]" id="mlsEmailErr"></em>
</label>
<label className="mls-field block mb-[15px]">
<span>Password</span>
<div className="relative">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off" />
<button type="button" className="absolute right-2 top-1/2 [transform:translateY(-50%)] bg-transparent border-0 text-[#94a3b8] cursor-pointer p-[5px] hover:text-[#475569]" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" strokeWidth="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a className="block text-right text-xs font-semibold text-[#6366f1] no-underline mb-[18px]" href="#">Forgot password?</a>
<button type="submit" className="mls-submit w-full bg-[#6366f1] text-[#fff] border-0 rounded-xl p-3.5 text-[14.5px] font-bold cursor-pointer font-[inherit] [transition:background_.15s] hover:bg-[#4f46e5]" id="mlsSubmit">Sign in</button>
</form>
<div className="mls-or flex items-center gap-2.5 my-5 mx-0 text-[#94a3b8] text-[11.5px]"><span>or continue with</span></div>
<div className="mls-social flex gap-2.5"><button type="button">Google</button><button type="button">Apple</button></div>
<p className="mls-foot text-center text-[12.5px] text-[#64748b] mt-5">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
</>
);
}<template>
<div class="mls-phone">
<div class="mls-screen">
<div class="mls-status"><span>9:41</span><span class="mls-batt"><i></i></span></div>
<div class="mls-body">
<div class="mls-brand"><div class="mls-logo"></div><span>Northwind</span></div>
<h1 class="mls-title">Welcome back</h1>
<p class="mls-sub">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label class="mls-field">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off">
<em class="mls-err" id="mlsEmailErr"></em>
</label>
<label class="mls-field">
<span>Password</span>
<div class="mls-pw">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off">
<button type="button" class="mls-eye" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a class="mls-forgot" href="#">Forgot password?</a>
<button type="submit" class="mls-submit" id="mlsSubmit">Sign in</button>
</form>
<div class="mls-or"><span>or continue with</span></div>
<div class="mls-social"><button type="button">Google</button><button type="button">Apple</button></div>
<p class="mls-foot">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let form;
let email;
let emailErr;
let pw;
let eye;
let submit;
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
onMounted(() => {
form = document.getElementById('mlsForm');
email = document.getElementById('mlsEmail');
emailErr = document.getElementById('mlsEmailErr');
pw = document.getElementById('mlsPw');
eye = document.getElementById('mlsEye');
submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.mls-phone{width:288px;height:600px;background:#0b1220;border-radius:46px;padding:12px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 0 2px #1e293b}
.mls-screen{width:100%;height:100%;border-radius:34px;overflow:hidden;background:#fff;color:#0f172a;display:flex;flex-direction:column}
.mls-status{display:flex;justify-content:space-between;align-items:center;padding:13px 24px 0;font-size:13px;font-weight:700}
.mls-batt{width:22px;height:11px;border:1.4px solid #0f172a;border-radius:3px;position:relative;display:inline-block}
.mls-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0}
.mls-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px}
.mls-body{flex:1;overflow-y:auto;padding:26px 24px 20px}
.mls-brand{display:flex;align-items:center;gap:9px;margin-bottom:30px}
.mls-logo{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#6366f1,#22d3ee)}
.mls-brand span{font-size:15px;font-weight:800}
.mls-title{font-size:25px;font-weight:800}
.mls-sub{font-size:13px;color:#94a3b8;margin:5px 0 22px}
.mls-field{display:block;margin-bottom:15px}
.mls-field>span{font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px}
.mls-field input{width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s}
.mls-field input:focus{border-color:#6366f1}
.mls-field.bad input{border-color:#ef4444}
.mls-err{display:block;font-size:11px;color:#ef4444;font-style:normal;margin-top:5px;min-height:13px}
.mls-pw{position:relative}
.mls-eye{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#94a3b8;cursor:pointer;padding:5px}
.mls-eye:hover{color:#475569}
.mls-forgot{display:block;text-align:right;font-size:12px;font-weight:600;color:#6366f1;text-decoration:none;margin-bottom:18px}
.mls-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:12px;padding:14px;font-size:14.5px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
.mls-submit:hover{background:#4f46e5}
.mls-submit.loading{background:#818cf8;pointer-events:none}
.mls-or{display:flex;align-items:center;gap:10px;margin:20px 0;color:#94a3b8;font-size:11.5px}
.mls-or::before,.mls-or::after{content:'';flex:1;height:1px;background:#e2e8f0}
.mls-social{display:flex;gap:10px}
.mls-social button{flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}
.mls-social button:hover{background:#f8fafc}
.mls-foot{text-align:center;font-size:12.5px;color:#64748b;margin-top:20px}
.mls-foot a{color:#6366f1;font-weight:700;text-decoration:none}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-mobile-login-screen',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="mls-phone">
<div class="mls-screen">
<div class="mls-status"><span>9:41</span><span class="mls-batt"><i></i></span></div>
<div class="mls-body">
<div class="mls-brand"><div class="mls-logo"></div><span>Northwind</span></div>
<h1 class="mls-title">Welcome back</h1>
<p class="mls-sub">Sign in to continue to your account</p>
<form id="mlsForm" novalidate>
<label class="mls-field">
<span>Email</span>
<input type="email" id="mlsEmail" value="" placeholder="[email protected]" autocomplete="off">
<em class="mls-err" id="mlsEmailErr"></em>
</label>
<label class="mls-field">
<span>Password</span>
<div class="mls-pw">
<input type="password" id="mlsPw" placeholder="••••••••" autocomplete="off">
<button type="button" class="mls-eye" id="mlsEye" aria-label="Show password">
<svg viewBox="0 0 24 24" width="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</label>
<a class="mls-forgot" href="#">Forgot password?</a>
<button type="submit" class="mls-submit" id="mlsSubmit">Sign in</button>
</form>
<div class="mls-or"><span>or continue with</span></div>
<div class="mls-social"><button type="button">Google</button><button type="button">Apple</button></div>
<p class="mls-foot">No account? <a href="#">Create one</a></p>
</div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.mls-phone{width:288px;height:600px;background:#0b1220;border-radius:46px;padding:12px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 0 2px #1e293b}
.mls-screen{width:100%;height:100%;border-radius:34px;overflow:hidden;background:#fff;color:#0f172a;display:flex;flex-direction:column}
.mls-status{display:flex;justify-content:space-between;align-items:center;padding:13px 24px 0;font-size:13px;font-weight:700}
.mls-batt{width:22px;height:11px;border:1.4px solid #0f172a;border-radius:3px;position:relative;display:inline-block}
.mls-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:#0f172a;border-radius:0 1px 1px 0}
.mls-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:65%;background:#0f172a;border-radius:1px}
.mls-body{flex:1;overflow-y:auto;padding:26px 24px 20px}
.mls-brand{display:flex;align-items:center;gap:9px;margin-bottom:30px}
.mls-logo{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#6366f1,#22d3ee)}
.mls-brand span{font-size:15px;font-weight:800}
.mls-title{font-size:25px;font-weight:800}
.mls-sub{font-size:13px;color:#94a3b8;margin:5px 0 22px}
.mls-field{display:block;margin-bottom:15px}
.mls-field>span{font-size:11.5px;font-weight:700;color:#64748b;display:block;margin-bottom:6px}
.mls-field input{width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:12px 13px;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s}
.mls-field input:focus{border-color:#6366f1}
.mls-field.bad input{border-color:#ef4444}
.mls-err{display:block;font-size:11px;color:#ef4444;font-style:normal;margin-top:5px;min-height:13px}
.mls-pw{position:relative}
.mls-eye{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#94a3b8;cursor:pointer;padding:5px}
.mls-eye:hover{color:#475569}
.mls-forgot{display:block;text-align:right;font-size:12px;font-weight:600;color:#6366f1;text-decoration:none;margin-bottom:18px}
.mls-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:12px;padding:14px;font-size:14.5px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
.mls-submit:hover{background:#4f46e5}
.mls-submit.loading{background:#818cf8;pointer-events:none}
.mls-or{display:flex;align-items:center;gap:10px;margin:20px 0;color:#94a3b8;font-size:11.5px}
.mls-or::before,.mls-or::after{content:'';flex:1;height:1px;background:#e2e8f0}
.mls-social{display:flex;gap:10px}
.mls-social button{flex:1;border:1.5px solid #e2e8f0;background:#fff;border-radius:11px;padding:11px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}
.mls-social button:hover{background:#f8fafc}
.mls-foot{text-align:center;font-size:12.5px;color:#64748b;margin-top:20px}
.mls-foot a{color:#6366f1;font-weight:700;text-decoration:none}
`]
})
export class MobileLoginScreenComponent implements AfterViewInit {
ngAfterViewInit(): void {
var form = document.getElementById('mlsForm');
var email = document.getElementById('mlsEmail');
var emailErr = document.getElementById('mlsEmailErr');
var pw = document.getElementById('mlsPw');
var eye = document.getElementById('mlsEye');
var submit = document.getElementById('mlsSubmit');
eye.addEventListener('click', function () {
var show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.style.color = show ? '#6366f1' : '';
});
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
form.addEventListener('submit', function (e) {
e.preventDefault();
var field = email.closest('.mls-field');
if (!validEmail(email.value)) {
field.classList.add('bad');
emailErr.textContent = email.value ? 'Enter a valid email address' : 'Email is required';
email.focus();
return;
}
field.classList.remove('bad'); emailErr.textContent = '';
if (pw.value.length < 6) { pw.focus(); pw.closest('.mls-field').classList.add('bad'); return; }
pw.closest('.mls-field').classList.remove('bad');
// Fake auth round-trip so the button shows its loading state.
submit.classList.add('loading'); submit.textContent = 'Signing in…';
setTimeout(function () { submit.classList.remove('loading'); submit.textContent = 'Signed in ✓'; }, 1300);
});
email.addEventListener('input', function () { email.closest('.mls-field').classList.remove('bad'); emailErr.textContent = ''; });
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { validEmail });
}
}