
How Do I Create a Custom Login Page for WordPress?
Every WordPress site, no matter how polished, drops visitors onto the exact same login screen: the same WordPress logo, the same grey box, the same generic styling that ships with core. For a client site, a membership community, or anything with a login-heavy front end, that mismatch is jarring, since it's the one moment a visitor is reminded they're using off-the-shelf software instead of your brand.
The good news is that WordPress gives you two very different levels of control here, and which one you need depends on your goal. If you just want your own logo, colors, and background on the existing login form, a handful of filters and a bit of CSS will get you there in minutes. If you want a fully custom-designed page, built with your own HTML and matched to your theme, you'll want to build a real login template instead. This guide walks through both, with working code for each.
Do You Need a Plugin, or Custom Code?
Before writing any code, it's worth being honest about which approach fits your situation:
- A plugin (like LoginPress or Custom Login Page Customizer) is the fastest route if you just want to swap the logo, background, and colors through a visual settings screen, and you're comfortable with an extra plugin on the site.
- A few lines of code is all you need if you want that same visual result (your own logo, colors, background) but would rather not add a dependency, or you're building this into a client theme that needs to work without any extra plugins.
- A fully custom template is worth the extra effort only if you need a login experience that doesn't look like
wp-login.phpat all, for example a login form embedded directly in your theme's design, on the front end, without the visitor ever seeing a separate WordPress-branded page.
We'll cover all three, starting with the lightest option.
Option 1: Style the Default Login Page With Code
This is the most common approach, and it's what most "branded login page" plugins are doing under the hood anyway. You hook into login_enqueue_scripts to load your own stylesheet, then override a few filters to change the logo's link and hover text.
Step 1: Create a Custom Login Stylesheet
Add a new file to your theme:
wp-content/themes/your-theme/css/custom-login.css
body.login {
background: #f4f2ff;
}
.login h1 a {
background-image: url('../images/custom-logo.png');
background-size: contain;
width: 260px;
height: 90px;
margin-bottom: 20px;
}
.login form {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(91, 79, 224, 0.12);
border: 1px solid #e3ddf7;
}
.login form .input,
.login input[type="text"],
.login input[type="password"] {
border-radius: 8px;
border-color: #d8d3f7;
}
.login #wp-submit {
background: #5b4fe0;
border-color: #5b4fe0;
border-radius: 8px;
}
.login #wp-submit:hover {
background: #4a3fc7;
border-color: #4a3fc7;
}
.login #backtoblog a,
.login #nav a {
color: #5b4fe0;
}
Step 2: Enqueue the Stylesheet on the Login Page
In your theme's functions.php (or, better, a must-use plugin so it survives a theme switch):
add_action( 'login_enqueue_scripts', function () {
wp_enqueue_style(
'custom-login-style',
get_stylesheet_directory_uri() . '/css/custom-login.css',
[],
'1.0.0'
);
} );
login_enqueue_scripts is the login page's equivalent of wp_enqueue_scripts, and it's the correct hook here because wp-login.php doesn't load your theme's regular stylesheets at all.
Step 3: Point the Logo Link at Your Own Site
By default, clicking the logo on the login page takes visitors to WordPress.org, which is a strange place to send someone who's trying to log into your site. Fix both the link and its hover text with two filters:
add_filter( 'login_headerurl', function () {
return home_url();
} );
add_filter( 'login_headertext', function () {
return get_bloginfo( 'name' );
} );
At this point, reload /wp-login.php and you'll see your own logo, your own colors, and a logo link that goes back to your homepage, all without touching WordPress core.
Option 2: Build a Fully Custom Login Template
If you need the login form to live inside your own theme's layout, header, and footer, rather than on a separate WordPress-branded page, you'll want a real page template. This is the right approach for membership sites, client portals, or anything where the login form needs to feel like a native part of the site rather than a detour to /wp-login.php.
Step 1: Create the Page Template
Add a new file to your theme:
wp-content/themes/your-theme/page-templates/custom-login.php
<?php
/**
* Template Name: Custom Login
*/
get_header();
if ( is_user_logged_in() ) {
wp_safe_redirect( admin_url() );
exit;
}
$login_error = '';
if ( isset( $_POST['custom_login_nonce'] ) && wp_verify_nonce( $_POST['custom_login_nonce'], 'custom_login_action' ) ) {
$creds = [
'user_login' => sanitize_user( $_POST['log'] ?? '' ),
'user_password' => $_POST['pwd'] ?? '',
'remember' => isset( $_POST['rememberme'] ),
];
$user = wp_signon( $creds, is_ssl() );
if ( is_wp_error( $user ) ) {
$login_error = $user->get_error_message();
} else {
wp_safe_redirect( home_url( '/dashboard' ) );
exit;
}
}
?>
<div class="custom-login-wrap">
<div class="custom-login-card">
<img src="<?php echo esc_url( get_stylesheet_directory_uri() . '/images/custom-logo.png' ); ?>" alt="<?php bloginfo( 'name' ); ?>" class="custom-login-logo">
<?php if ( $login_error ) : ?>
<p class="custom-login-error"><?php echo wp_kses_post( $login_error ); ?></p>
<?php endif; ?>
<form method="post" action="">
<?php wp_nonce_field( 'custom_login_action', 'custom_login_nonce' ); ?>
<label for="log">Username or Email</label>
<input type="text" name="log" id="log" required>
<label for="pwd">Password</label>
<input type="password" name="pwd" id="pwd" required>
<label class="custom-login-remember">
<input type="checkbox" name="rememberme" value="forever"> Remember me
</label>
<button type="submit">Log In</button>
<a class="custom-login-lost" href="<?php echo esc_url( wp_lostpassword_url() ); ?>">Lost your password?</a>
</form>
</div>
</div>
<?php get_footer(); ?>
A few important details in this snippet:
wp_verify_nonce()confirms the form was actually submitted from this page and not forged elsewhere, which is essential any time you're handling authentication yourself.wp_signon()is the same core functionwp-login.phpuses internally, so passwords are checked the normal, secure WordPress way. You are never comparing passwords yourself.is_wp_error()catches bad credentials and displays WordPress's own error message, so users still get a clear "incorrect password" style response.wp_lostpassword_url()links back to the real password reset flow, so you don't have to rebuild that piece from scratch.
Step 2: Assign the Template to a Page
In the WordPress dashboard, create a new page (for example, titled "Login"), and in the Page Attributes panel on the right, choose Custom Login as the template. Publish it, and your custom login form is now live at that page's URL, wrapped in your theme's normal header and footer.
Step 3: Style the Login Card
Add matching styles to your theme's main stylesheet:
.custom-login-wrap {
display: flex;
justify-content: center;
padding: 80px 20px;
}
.custom-login-card {
width: 100%;
max-width: 380px;
padding: 40px;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 8px 24px rgba(91, 79, 224, 0.12);
}
.custom-login-logo {
display: block;
max-width: 160px;
margin: 0 auto 24px;
}
.custom-login-card label {
display: block;
margin: 16px 0 6px;
font-weight: 600;
}
.custom-login-card input[type="text"],
.custom-login-card input[type="password"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #d8d3f7;
border-radius: 8px;
}
.custom-login-card button {
width: 100%;
margin-top: 20px;
padding: 12px;
border: none;
border-radius: 8px;
background: #5b4fe0;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.custom-login-error {
color: #c74848;
font-size: 14px;
}
Step 4: Redirect the Default Login Page (Optional)
If you want your custom page to fully replace wp-login.php for logged-out visitors, rather than existing alongside it, redirect the default page to your new one:
add_action( 'login_init', function () {
if ( ! isset( $_GET['action'] ) ) {
wp_safe_redirect( home_url( '/login' ) );
exit;
}
} );
The isset( $_GET['action'] ) check is important here: it lets requests like password resets (?action=rp) and logout (?action=logout) continue to use WordPress core's own handling, and only redirects the plain login form itself.
Redirecting Users After Login
Whichever approach you use, you'll often want different user roles to land somewhere specific after logging in, for example, sending administrators to the dashboard but sending regular members to their account page. The login_redirect filter handles this for both the default login page and a custom form built with wp_signon():
add_filter( 'login_redirect', function ( $redirect_to, $requested_redirect_to, $user ) {
if ( is_wp_error( $user ) ) {
return $redirect_to;
}
if ( in_array( 'administrator', (array) $user->roles, true ) ) {
return admin_url();
}
return home_url( '/account' );
}, 10, 3 );
Keeping a Custom Login Page Secure
A custom-designed login form is only as secure as the code handling it, so keep these in mind:
- Never skip the nonce check. Without
wp_verify_nonce(), your form has no protection against cross-site request forgery. - Let
wp_signon()do the authentication. Don't query the database for the user's password yourself; WordPress's own hashing and rate-limiting logic lives inside that function. - Add a login attempt limiter, such as Limit Login Attempts Reloaded, since a custom form is just as exposed to brute-force attempts as the default one.
- Consider pairing this with a custom login URL, especially if you've redirected
wp-login.phpaway entirely, so the old default URL doesn't sit there as an unused but still-functional target.
Frequently Asked Questions (FAQ) About Custom WordPress Login Pages
Yes, if you try to use both at once for the same thing. A plugin that styles wp-login.php and a custom login_enqueue_scripts hook doing the same job will fight over the same CSS. Pick one approach: a plugin for a quick visual refresh, or the code above if you want full control without an extra dependency.
No. Both approaches in this guide reuse WordPress's existing password reset system: the default login page keeps it automatically, and the custom template links to it directly via wp_lostpassword_url(). You only need to build a custom reset flow if you want that page redesigned too, which follows a similar pattern using check_password_reset_key() and reset_password().
Yes, it's the same core function WordPress itself calls when you submit the default login form, so passwords are verified with the same hashing and the same security filters (including anything a security plugin adds to wp_authenticate). You should never write your own password comparison logic.
Yes, but each site in the network needs its own copy of the page template and any redirect logic, since login_init and the custom page template apply per-site unless you specifically write network-wide logic. Test the redirect on each site rather than assuming it propagates automatically.
The most common cause is enqueuing the stylesheet on the wrong hook. wp-login.php does not load wp_enqueue_scripts at all, so the stylesheet must be attached to login_enqueue_scripts specifically, as shown in Option 1. Also confirm the file path passed to get_stylesheet_directory_uri() actually matches where you saved the CSS file.
A page template (as shown above) is simpler for most sites, since you assign it once and it fully controls the page. A shortcode version of the same form logic is worth building if you need to drop the login form into multiple existing pages, or inside a widget area, rather than dedicating one page to it.
Conclusion
Most sites don't need to reinvent the login page from scratch, and styling the default one with login_enqueue_scripts and a couple of filters gets you a fully branded result in under an hour, with no plugin required. Reach for a fully custom template only when you specifically need the login form embedded in your own theme's layout, since that path also means taking on the responsibility of handling authentication, errors, and redirects correctly yourself, using WordPress's own wp_signon() and nonce functions rather than reinventing them.
Whichever route fits your project, keep the same security basics in place that protect the default login page: nonces, rate limiting, and core's own authentication functions, so a nicer-looking login form doesn't end up being a less secure one.
Here are a few additional resources if you want to go deeper:
- WordPress Developer Reference: wp_signon() — the full parameter list and return values for the core authentication function used above.
- WordPress Developer Reference: login_enqueue_scripts — the hook reference for loading assets on the default login page.
- LoginPress on WordPress.org — a solid plugin option if you'd rather configure a branded login page visually than maintain the code yourself.


