Tutorials / / 5 min read
How to create a login/register form with Tailwind CSS: JavaScript and Alpine.js
Build a login form with client-side validation using Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Email check, password rules, show/hide toggle.
This Thursday we are building a login form with client-side validation. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is an authentication form?
Authentication forms are a crucial part of web and application security: they are how users prove who they are before getting access. Ours is a classic email and password login with client-side validation, a show/hide password toggle, and inline error messages.
Use cases
- Traditional username and password: the standard login most apps ship first.
- Multi-factor authentication: pair the password with a code from SMS or an authenticator app.
- Social logins and SSO: let users sign in with existing accounts or one set of corporate credentials.
- Magic links: skip the password entirely and email the user a login link.
The markup
id="loginFormContainer": the container that wraps the form.<form id="loginForm">: the form we listen to for submissions.id="login_email"andid="emailError": the email input and the error message shown when the email is not filled in.id="login_password"andid="passwordError": the password input and the error message shown when the password does not meet the requirements.id="togglePassword": the button that toggles the visibility of the password input field.
Classes are omitted for brevity and clarity.
<div id="loginFormContainer">
<form id="loginForm">
<div>
<label for="login_email">Email</label>
<input
type="email"
id="login_email"
placeholder="Enter your email"
required
/>
<p id="emailError">Email is required</p>
</div>
<div>
<label for="login_password">Password</label>
<div class="relative">
<input
type="password"
id="login_password"
placeholder="Enter your password"
required
/>
<span id="togglePassword">Show</span>
</div>
<p>
Password must contain at least one capital letter and a special
character.
</p>
<p id="passwordError">Password does not meet requirements</p>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</div>The script
document.addEventListener("DOMContentLoaded", () => {: waits until the page is loaded, then grabs the form, the inputs, the error messages, and the toggle button.const passwordPattern = /^(?=.*[A-Z])(?=.*\W).+$/;: the regular expression used to validate the password, andlet showPassword = false;stores the state of the toggle.togglePassword.addEventListener("click", () => {: flipsshowPassword, switches the input betweentextandpassword, and updates the button label toHideorShow.loginForm.addEventListener("submit", (event) => {: prevents the default submission, then shows or hidesemailErrordepending on whether the email is filled in, and does the same forpasswordErrorusing the pattern.- When the email is present and the password passes the pattern, we perform the login action, here just an
alert("Login successful").
document.addEventListener("DOMContentLoaded", () => {
const loginForm = document.getElementById("loginForm");
const loginEmail = document.getElementById("login_email");
const loginPassword = document.getElementById("login_password");
const emailError = document.getElementById("emailError");
const passwordError = document.getElementById("passwordError");
const togglePassword = document.getElementById("togglePassword");
const passwordPattern = /^(?=.*[A-Z])(?=.*\W).+$/;
let showPassword = false;
togglePassword.addEventListener("click", () => {
showPassword = !showPassword;
loginPassword.type = showPassword ? "text" : "password";
togglePassword.textContent = showPassword ? "Hide" : "Show";
});
loginForm.addEventListener("submit", (event) => {
event.preventDefault();
if (!loginEmail.value) {
emailError.classList.remove("hidden");
} else {
emailError.classList.add("hidden");
}
if (!passwordPattern.test(loginPassword.value)) {
passwordError.classList.remove("hidden");
} else {
passwordError.classList.add("hidden");
}
if (loginEmail.value && passwordPattern.test(loginPassword.value)) {
// Perform login action here
alert("Login successful");
}
});
});The Alpine.js version
Same component, no separate script: the state lives in x-data.
x-data="{ loginEmail: '', loginPassword: '', passwordPattern: ..., showPassword: false }": holds the email, the password, the validation pattern, and the visibility toggle.x-on:submit.prevent="login": prevents the form from submitting the default way.x-model="loginEmail": binds the email input to theloginEmailvariable, andx-show="!loginEmail"shows the error message if the email is not filled in.:type="showPassword ? 'text' : 'password'",x-text="showPassword ? 'Hide' : 'Show'", and@click="showPassword = !showPassword": together they handle the show/hide password toggle.x-show="loginPassword && !passwordPattern.test(loginPassword)": shows the error message only when the password does not meet the requirements.- The text saying the password must contain a capital letter and a special character is always visible to avoid confusion, that’s part of good UX.
<div
x-data="{ loginEmail: '', loginPassword: '', passwordPattern: /^(?=.*[A-Z])(?=.*\W).+$/, showPassword: false }"
>
<form x-on:submit.prevent="login">
<div>
<label for="login_email">Email</label>
<input
type="email"
id="login_email"
x-model="loginEmail"
placeholder="Enter your email"
required
/>
<p x-show="!loginEmail" class="text-red-500 ">Email is required</p>
</div>
<div>
<label for="login_password">Password</label>
<div class="relative">
<input
:type="showPassword ? 'text' : 'password'"
id="login_password"
x-model="loginPassword"
placeholder="Enter your password"
required
/>
<span
x-text="showPassword ? 'Hide' : 'Show'"
@click="showPassword = !showPassword"
>Show</span
>
</div>
<p class="...">
Password must contain at least one capital letter and a special
character.
</p>
<p
x-show="loginPassword && !passwordPattern.test(loginPassword)"
class="text-red-500..."
>
Password does not meet requirements
</p>
</div>
<div class="mt-4">
<button type="submit">Login</button>
</div>
</form>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If your project already ships Alpine, the x-data version keeps the whole form self-contained in the markup. If not, the vanilla script gives you the same validation without an extra dependency.
Conclusion
We covered creating the form, handling submissions, validating user input, and showing error messages, in both flavors. Remember to keep your login form responsive, user-friendly, and secure, and test it thoroughly before shipping.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza