Build an accessible React password show or hide toggle
Build an accessible React password field with a show or hide toggle, covering UX, a11y, TypeScript, SSR, and integrations with MUI and Chakra.
Image used for representation purposes only.
Why a show or hide toggle matters
A password reveal control looks simple, yet it touches UX, accessibility, privacy, internationalization, and even server rendering. In this guide you will build a robust React password input with a show or hide toggle, understand the trade‑offs of controlled vs uncontrolled inputs, and learn how to integrate the pattern with popular UI libraries.
UX and accessibility essentials
A good toggle does more than swap the input type.
- Use a real
<button type='button'>for the toggle; never use a link or a span with click handlers. - Provide an accessible name that updates with state, for example
Show passwordandHide password. - Add
aria-pressedto communicate the toggle state to assistive tech. - Preserve label association with
labelandhtmlFor. - Keep the toggle reachable with keyboard:
Tabto focus,EnterorSpaceto activate. - Do not rely on an icon alone; combine an icon with visible text or a visually hidden label.
- Respect user privacy; default to hidden and consider opt‑in auto‑hide after inactivity.
Minimal, accessible implementation (controlled)
This example keeps the input controlled by React state, which is convenient for form validation and integrations. It also demonstrates clear, accessible button text and aria-pressed.
import React from 'react';
export function PasswordInputControlled() {
const [value, setValue] = React.useState('');
const [revealed, setRevealed] = React.useState(false);
return (
<div className='field'>
<label htmlFor='password'>Password</label>
<div className='input-group'>
<input
id='password'
name='password'
type={revealed ? 'text' : 'password'}
value={value}
onChange={(e) => setValue(e.target.value)}
autoComplete='current-password'
inputMode='text'
spellCheck={false}
aria-describedby='password-help'
/>
<button
type='button'
className='toggle'
onClick={() => setRevealed((r) => !r)}
aria-pressed={revealed}
aria-label={revealed ? 'Hide password' : 'Show password'}
>
{revealed ? 'Hide' : 'Show'}
</button>
</div>
<p id='password-help' className='help'>
Use at least 12 characters with a mix of letters, numbers, and symbols.
</p>
</div>
);
}
Optional, minimal CSS to align the toggle with the input:
.field { max-width: 420px; }
.input-group { position: relative; display: flex; }
.input-group input { flex: 1; padding-right: 4rem; }
.input-group .toggle {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none; border: 0; cursor: pointer;
padding: 0.25rem 0.5rem; color: #444;
}
.input-group .toggle:focus { outline: 2px solid #2684ff; outline-offset: 2px; }
Reusable component with TypeScript and auto‑hide
This component supports both controlled and uncontrolled usage and can auto‑hide after a configurable period. It forwards a ref for form libraries.
import React, { forwardRef } from 'react';
type PasswordFieldProps = {
id?: string;
name?: string;
label?: string;
value?: string;
defaultValue?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
autoComplete?: 'current-password' | 'new-password';
revealText?: string; // i18n
hideText?: string; // i18n
autoHideAfterMs?: number; // auto conceal delay
disabled?: boolean;
};
export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
(
{
id = 'password',
name = 'password',
label = 'Password',
value,
defaultValue,
onChange,
autoComplete = 'current-password',
revealText = 'Show password',
hideText = 'Hide password',
autoHideAfterMs,
disabled,
},
ref
) => {
const [revealed, setRevealed] = React.useState(false);
// Optional auto-hide timer
React.useEffect(() => {
if (!revealed || !autoHideAfterMs) return;
const t = setTimeout(() => setRevealed(false), autoHideAfterMs);
return () => clearTimeout(t);
}, [revealed, autoHideAfterMs]);
return (
<div className='field'>
<label htmlFor={id}>{label}</label>
<div className='input-group'>
<input
ref={ref}
id={id}
name={name}
type={revealed ? 'text' : 'password'}
value={value}
defaultValue={defaultValue}
onChange={onChange}
autoComplete={autoComplete}
inputMode='text'
spellCheck={false}
disabled={disabled}
/>
<button
type='button'
className='toggle'
onClick={() => setRevealed((r) => !r)}
aria-pressed={revealed}
aria-label={revealed ? hideText : revealText}
disabled={disabled}
>
{revealed ? 'Hide' : 'Show'}
</button>
</div>
</div>
);
}
);
Usage examples:
// Controlled
const [pwd, setPwd] = React.useState('');
<PasswordField value={pwd} onChange={(e) => setPwd(e.target.value)} />;
// Uncontrolled
<PasswordField defaultValue='' />;
// With auto-hide after 10 seconds
<PasswordField autoHideAfterMs={10_000} />;
// i18n labels
<PasswordField revealText='Mostrar contraseña' hideText='Ocultar contraseña' />;
Uncontrolled variant for reduced in‑memory exposure
If you prefer not to mirror the password in React state, use an uncontrolled input. The reveal toggle still works because it only changes the type attribute.
function PasswordUncontrolled() {
const inputRef = React.useRef<HTMLInputElement>(null);
const [revealed, setRevealed] = React.useState(false);
const submit = () => {
const value = inputRef.current?.value ?? '';
// send value to your API securely
};
return (
<div className='field'>
<label htmlFor='pwd-u'>Password</label>
<div className='input-group'>
<input id='pwd-u' ref={inputRef} type={revealed ? 'text' : 'password'} autoComplete='current-password' />
<button type='button' aria-pressed={revealed} aria-label={revealed ? 'Hide password' : 'Show password'} onClick={() => setRevealed(!revealed)}>
{revealed ? 'Hide' : 'Show'}
</button>
</div>
<button onClick={submit}>Sign in</button>
</div>
);
}
SSR and hydration notes
- Default the toggle to concealed on the server to avoid mismatches. If you must derive the initial state from the client (for example, user preference), set a concealed default and update after
useEffectruns. - Icon‑only buttons can shift on hydration if font icons load late; prefer inline SVG.
// Safe default on server
const [revealed, setRevealed] = React.useState(false); // matches server and client
Integration with popular UI libraries
You can keep the same logic and wire it to your component library of choice.
Material UI (MUI)
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
function MuiPassword() {
const [revealed, setRevealed] = React.useState(false);
return (
<TextField
label='Password'
type={revealed ? 'text' : 'password'}
autoComplete='current-password'
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton
onClick={() => setRevealed((r) => !r)}
aria-label={revealed ? 'Hide password' : 'Show password'}
aria-pressed={revealed}
edge='end'
>
{revealed ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
variant='outlined'
/>
);
}
Chakra UI
import { Input, InputGroup, InputRightElement, IconButton, VisuallyHidden } from '@chakra-ui/react';
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
function ChakraPassword() {
const [revealed, setRevealed] = React.useState(false);
return (
<InputGroup>
<Input type={revealed ? 'text' : 'password'} autoComplete='current-password' placeholder='Password' />
<InputRightElement>
<IconButton
aria-label={revealed ? 'Hide password' : 'Show password'}
aria-pressed={revealed}
onClick={() => setRevealed((r) => !r)}
icon={revealed ? <ViewOffIcon /> : <ViewIcon />}
variant='ghost'
/>
<VisuallyHidden>{revealed ? 'Hide password' : 'Show password'}</VisuallyHidden>
</InputRightElement>
</InputGroup>
);
}
Security and privacy considerations
- Revealing switches the visual mask only. It does not change whether scripts can read the value; assume any client script with access can read it. Focus on preventing shoulder surfing and screen capture.
- Default to hidden on load. Make reveal opt‑in and consider an optional auto‑hide timer.
- Do not disable paste; it harms usability and password manager flows without raising actual security.
- Use the appropriate
autoCompletevalue:current-passwordfor sign‑in,new-passwordfor sign‑up or change flows. - Avoid logging input values or echoing them in error boundaries.
- Ensure the toggle does not submit the form: the button must be
type='button'.
Internationalization tips
- Do not hardcode English. Parameterize the reveal and hide labels or read them from your i18n layer.
- Keep labels concise; they are read frequently by screen readers.
- If you swap the icon when revealed, maintain consistent button width to prevent layout shift with longer translations.
Testing the toggle with React Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PasswordField } from './PasswordField';
test('toggles input type and accessible name', async () => {
render(<PasswordField />);
const button = screen.getByRole('button', { name: /show password/i });
const input = screen.getByLabelText(/password/i) as HTMLInputElement;
expect(input.type).toBe('password');
await userEvent.click(button);
expect(input.type).toBe('text');
expect(button).toHaveAttribute('aria-pressed', 'true');
expect(button).toHaveAccessibleName(/hide password/i);
});
Common pitfalls to avoid
- Icon only with no accessible name. Always provide a readable label.
- Using a checkbox instead of a button; the action is a transient reveal, not a persisted setting.
- Forgetting
type='button'on the toggle, which can cause accidental form submissions. - Managing visibility entirely in CSS (for example, text‑security tricks) instead of switching the
typeattribute. - Placing the toggle after the submit button in the tab order; position it as an end‑adornment of the field.
Quick checklist
- Button with changing accessible name and
aria-pressed. - Keyboard operable and focus visible.
- Default concealed; optional auto‑hide.
- Works with password managers via correct
autoComplete. - Tested for both controlled and uncontrolled use.
Wrap‑up
A well‑designed show or hide password toggle is a small but high‑impact feature. By combining clear labels, proper semantics, and thoughtful privacy defaults, you can deliver a field that is pleasant to use, works with assistive technology, and integrates cleanly across your design system. The snippets above give you everything you need to implement it in plain React or with libraries like MUI and Chakra, all while keeping accessibility and security front and center.
Related Posts
Building an Accessible, Animated Expandable Card in React
Build an accessible, animated React expandable card with TypeScript. Covers ARIA, controlled/uncontrolled state, CSS and Framer Motion, and testing.
Building a Robust React Aspect Ratio Container in React (with Fallbacks)
Build a robust React aspect-ratio container with CSS and fallbacks. Includes TypeScript code, usage patterns, accessibility, testing, and performance tips.
Build a Robust React OTP Input Verification Component
Build an accessible, mobile-friendly React OTP verification component with great UX, paste handling, and security—complete with TypeScript code.