The show/hide password toggle is one of the most useful small interactions you can add to a login or signup form. Typing a complex password into a masked field and then guessing whether you made a typo is frustrating. One button fixes it entirely.
The implementation is genuinely simple — just a few lines of JavaScript — but there are enough details around accessibility, icon handling, and edge cases to make a thorough walkthrough worth reading.
How It Works
A password input field has its type attribute set to "password", which masks the characters. Setting type to "text" reveals them. Toggling between these two values is all the show/hide logic requires.
const input = document.getElementById("password");
// Show password
input.type = "text";
// Hide password
input.type = "password";
That is the entire mechanism. Everything else is UI and accessibility.
The Basic Implementation
<div class="input-wrapper">
<input type="password" id="password" placeholder="Enter your password" />
<button type="button" id="toggle-btn">Show</button>
</div>
const passwordInput = document.getElementById("password");
const toggleBtn = document.getElementById("toggle-btn");
toggleBtn.addEventListener("click", function () {
if (passwordInput.type === "password") {
passwordInput.type = "text";
toggleBtn.textContent = "Hide";
} else {
passwordInput.type = "password";
toggleBtn.textContent = "Show";
}
});
Click the button — the input type flips and the button label updates. This is the complete implementation for a no-frills use case.
Using an Eye Icon Instead of Text
Most production implementations use an eye icon rather than text. The icon changes between an open eye (password visible) and a closed eye (password hidden).
Using SVG icons inline keeps you dependency-free:
<div class="input-wrapper">
<input type="password" id="password" placeholder="Enter your password" />
<button type="button" id="toggle-btn" aria-label="Show password">
<svg id="icon-show" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg id="icon-hide" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
</button>
</div>
const passwordInput = document.getElementById("password");
const toggleBtn = document.getElementById("toggle-btn");
const iconShow = document.getElementById("icon-show");
const iconHide = document.getElementById("icon-hide");
toggleBtn.addEventListener("click", function () {
const isPassword = passwordInput.type === "password";
passwordInput.type = isPassword ? "text" : "password";
iconShow.style.display = isPassword ? "none" : "block";
iconHide.style.display = isPassword ? "block" : "none";
toggleBtn.setAttribute(
"aria-label",
isPassword ? "Hide password" : "Show password"
);
});
When the password is visible, the crossed-out eye shows. When hidden, the open eye shows. The aria-label updates to describe what the button will do next — which is the correct pattern for toggle buttons.
A Cleaner Toggle with classList
Instead of managing two separate SVGs, one icon with a CSS class toggled is cleaner:
<button type="button" id="toggle-btn" aria-label="Show password">
<span class="eye-icon" id="eye-icon">👁</span>
</button>
.eye-icon.hidden-eye::after {
content: "🙈";
}
.eye-icon.hidden-eye {
font-size: 0; /* Hide the original character */
}
Or more simply, just swap the emoji directly in JavaScript:
const passwordInput = document.getElementById("password");
const toggleBtn = document.getElementById("toggle-btn");
const eyeIcon = document.getElementById("eye-icon");
toggleBtn.addEventListener("click", function () {
const isPassword = passwordInput.type === "password";
passwordInput.type = isPassword ? "text" : "password";
eyeIcon.textContent = isPassword ? "🙈" : "👁";
toggleBtn.setAttribute(
"aria-label",
isPassword ? "Hide password" : "Show password"
);
});
Pick whichever approach fits your icon system. The toggle logic is identical regardless.
Full Working Example With CSS
Here is the complete implementation — HTML, CSS, and JavaScript together:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Password Toggle</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f1f5f9;
font-family: "Segoe UI", sans-serif;
}
.input-wrapper {
position: relative;
width: 320px;
}
#password {
width: 100%;
padding: 12px 46px 12px 14px;
border: 1.5px solid #d1d5db;
border-radius: 10px;
font-size: 1rem;
color: #1e293b;
outline: none;
}
#password:focus {
border-color: #3b82f6;
}
#toggle-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 4px;
line-height: 1;
color: #64748b;
transition: color 0.2s;
}
#toggle-btn:hover {
color: #1e293b;
}
</style>
</head>
<body>
<div class="input-wrapper">
<input
type="password"
id="password"
placeholder="Enter your password"
autocomplete="current-password"
/>
<button type="button" id="toggle-btn" aria-label="Show password">
👁
</button>
</div>
<script>
const passwordInput = document.getElementById("password");
const toggleBtn = document.getElementById("toggle-btn");
toggleBtn.addEventListener("click", function () {
const isPassword = passwordInput.type === "password";
passwordInput.type = isPassword ? "text" : "password";
this.textContent = isPassword ? "🙈" : "👁";
this.setAttribute("aria-label", isPassword ? "Hide password" : "Show password");
});
</script>
</body>
</html>
Paste this into an HTML file and it works immediately. The password field toggles on click, the icon swaps, and the aria-label updates for screen readers.
Handling Multiple Password Fields
Signup forms often have two password fields — password and confirm password. Each needs its own toggle but the logic can be shared:
<div class="input-wrapper">
<input type="password" id="password" placeholder="Password" />
<button type="button" class="toggle-btn" data-target="password" aria-label="Show password">👁</button>
</div>
<div class="input-wrapper">
<input type="password" id="confirm-password" placeholder="Confirm password" />
<button type="button" class="toggle-btn" data-target="confirm-password" aria-label="Show password">👁</button>
</div>
document.querySelectorAll(".toggle-btn").forEach(btn => {
btn.addEventListener("click", function () {
const targetId = this.dataset.target;
const input = document.getElementById(targetId);
const isPassword = input.type === "password";
input.type = isPassword ? "text" : "password";
this.textContent = isPassword ? "🙈" : "👁";
this.setAttribute("aria-label", isPassword ? "Hide password" : "Show password");
});
});
The data-target attribute tells each button which input to control. querySelectorAll finds all toggle buttons and attaches the same handler to each. Adding more password fields to the page requires no JavaScript changes — just add the markup with the right data-target.
Auto-Hiding After a Delay
Some applications show the password briefly and then re-mask it automatically:
let hideTimeout;
toggleBtn.addEventListener("click", function () {
const isPassword = passwordInput.type === "password";
clearTimeout(hideTimeout); // Cancel any pending auto-hide
if (isPassword) {
// Show password
passwordInput.type = "text";
this.textContent = "🙈";
this.setAttribute("aria-label", "Hide password");
// Auto-hide after 3 seconds
hideTimeout = setTimeout(() => {
passwordInput.type = "password";
toggleBtn.textContent = "👁";
toggleBtn.setAttribute("aria-label", "Show password");
}, 3000);
} else {
// Hide password manually
passwordInput.type = "password";
this.textContent = "👁";
this.setAttribute("aria-label", "Show password");
}
});
clearTimeout() before setting a new timer prevents multiple pending timeouts from stacking if the user clicks rapidly.
Keeping the Cursor Position After Toggle
When the input type changes in some browsers, the cursor jumps to the end of the text. If maintaining cursor position matters for your use case:
toggleBtn.addEventListener("click", function () {
const start = passwordInput.selectionStart;
const end = passwordInput.selectionEnd;
const isPassword = passwordInput.type === "password";
passwordInput.type = isPassword ? "text" : "password";
// Restore cursor position
passwordInput.setSelectionRange(start, end);
this.textContent = isPassword ? "🙈" : "👁";
this.setAttribute("aria-label", isPassword ? "Hide password" : "Show password");
});
selectionStart and selectionEnd capture the cursor position before the type switch. setSelectionRange() restores it after.
Accessibility Requirements
A toggle button that visually shows an icon but has no accessible name is invisible to screen reader users. The aria-label attribute provides the accessible name and should update with every toggle.
// When showing the password:
toggleBtn.setAttribute("aria-label", "Hide password");
// When hiding the password:
toggleBtn.setAttribute("aria-label", "Show password");
The label describes what the button will do next, not the current state. A button that currently shows the password should be labeled “Hide password” — clicking it hides.
The button must be type="button" to prevent it from submitting the form when inside a <form> element. A button without a type defaults to type="submit", which would trigger form submission on click.
Tips for Password Toggle Buttons
Always use type="button". Inside a form, a button without an explicit type submits the form. The toggle must never trigger form submission.
Update aria-label on every toggle. Icon-only buttons with no accessible name are invisible to screen reader users. The label is the only way they know what the button does.
Position the button absolutely inside the input wrapper. An icon overlapping the right side of the input is the standard pattern users expect. Avoid placing the button outside the input — it breaks the visual association.
Do not auto-hide without user control. Auto-hiding after a timer can be convenient but also frustrating if the user is still reading. Offer it as a feature but always let users toggle manually too.
Keep focus on the input after toggling. If the user clicks the toggle while typing, they should not have to click back into the input to continue. Some implementations call passwordInput.focus() after toggling.
toggleBtn.addEventListener("click", function () {
// ... toggle logic ...
passwordInput.focus();
});
The show/hide password toggle comes down to three lines — flip input.type between "password" and "text", update the icon, and update the aria-label. Everything else is refinement.
Add the multi-field pattern when you have a confirm password field. Add the auto-hide timer when the use case calls for it. Keep type="button" and aria-label on every implementation.
It is a small feature that makes a real difference in how users experience your forms.