How to customize the login screen with PHP and enqueue JS in WordPress

Contents

Overview

This article is a complete, detailed tutorial on how to customize the WordPress login screen using PHP and how to properly enqueue JavaScript (and CSS) for the login page. It covers where to put your code (plugin vs theme), how to enqueue assets only for the login screen, how to pass data from PHP to JavaScript securely, examples that include AJAX-driven login handling, best practices, security considerations, performance tips, and common troubleshooting notes. Every code example is provided so you can copy and paste into a plugin or theme.

Why customize the login screen?

  • Branding: match your client or site identity (logo, colors, background, copy).
  • UX improvements: provide inline help, password visibility toggles, enhanced error messages, or client-specific form fields.
  • Security/flows: add client-side features like reCAPTCHA, progressive MFA prompts, or AJAX-friendly feedback while keeping server-side verification.
  • Performance and maintainability: avoid editing core WordPress files — use a plugin or a child theme to keep changes safe across updates.

Where to put this code

  1. As a plugin (recommended): Easier to move between sites and keep separate from theme logic. Use a single-file plugin for small changes or a structured plugin for larger functionality.
  2. In your child themes functions.php: Quick and simple for theme-specific branding. Avoid editing parent theme files.

Key WordPress hooks and filters for the login screen

  • Actions: login_enqueue_scripts, login_head, login_footer, login_form_top, login_form_middle
  • Filters: login_message, login_headerurl, login_headertext (or login_headertitle in older installs — include both as fallback), login_errors
  • AJAX hooks (for unauthenticated requests): wp_ajax_nopriv_{action}

Principles for enqueuing scripts and styles on the login screen

  1. Use login_enqueue_scripts action to register and enqueue scripts/styles. It runs only on the login page, so assets wont load on the frontend or wp-admin unnecessarily.
  2. Use wp_register_script and wp_enqueue_script. Use file modification time (filemtime) to version files in development so browsers refresh automatically.
  3. Use wp_localize_script or wp_add_inline_script to securely pass data (ajax URLs, nonces, localized strings) from PHP to JavaScript.
  4. Always escape or validate inputs server-side never trust client-side checks.

Minimal plugin scaffold (single file)

Place this in wp-content/plugins/custom-login/custom-login.php and activate the plugin.

lt?php
/
  Plugin Name: Custom Login Enhancements
  Description: Customizes the login screen and enqueues JS/CSS with secure AJAX examples.
  Version:     1.0.0
  Author:      Your Name
  Text Domain: custom-login
 /

if ( ! defined( ABSPATH ) ) {
    exit
}

/
  Enqueue assets for the login screen.
 /
function cle_login_enqueue_assets() {
    dir = plugin_dir_url( __FILE__ )
    path = plugin_dir_path( __FILE__ )

    // Register styles and scripts
    wp_register_style(
        cle-login-style,
        dir . assets/css/login.css,
        array(),
        filemtime( path . assets/css/login.css )
    )

    wp_register_script(
        cle-login-js,
        dir . assets/js/login.js,
        array(), // add jquery if your script requires it
        filemtime( path . assets/js/login.js ),
        true
    )

    // Localize data for JS: ajax_url and a nonce
    wp_localize_script(
        cle-login-js,
        cleLogin,
        array(
            ajax_url => admin_url( admin-ajax.php ),
            nonce    => wp_create_nonce( cle-login-nonce ),
            i18n     => array(
                ajax_error => __( AJAX error, custom-login ),
            ),
        )
    )

    // Enqueue them
    wp_enqueue_style( cle-login-style )
    wp_enqueue_script( cle-login-js )
}
add_action( login_enqueue_scripts, cle_login_enqueue_assets )

/
  Change the login logo URL to the site home.
 /
function cle_login_logo_url() {
    return home_url()
}
add_filter( login_headerurl, cle_login_logo_url )

/
  Change the login logo title attribute (hover text).
 /
function cle_login_logo_title() {
    return get_bloginfo( name )
}
add_filter( login_headertext, cle_login_logo_title )
add_filter( login_headertitle, cle_login_logo_title ) // fallback for older installs

/
  Example AJAX handler for a custom login attempt (unauthenticated).
 /
function cle_ajax_login() {
    check_ajax_referer( cle-login-nonce, security )

    creds = array()
    creds[user_login]    = isset( _POST[username] ) ? sanitize_text_field( wp_unslash( _POST[username] ) ) : 
    creds[user_password] = isset( _POST[password] ) ? _POST[password] : 
    creds[remember]      = isset( _POST[remember] )  _POST[remember] === 1

    require_once ABSPATH . wp-includes/pluggable.php
    user = wp_signon( creds, is_ssl() )

    if ( is_wp_error( user ) ) {
        wp_send_json_error( array( message => user->get_error_message() ), 403 )
    }

    // On success: return redirect URL or success flag
    wp_send_json_success( array( redirect => admin_url() ) )
}
add_action( wp_ajax_nopriv_cle_ajax_login, cle_ajax_login )

Files referenced by the plugin

  1. assets/css/login.css — styles for the login page
  2. assets/js/login.js — JavaScript that augments the login screen and performs AJAX

Sample CSS (assets/css/login.css)

This CSS changes the logo, background, and the form styles. Save it as assets/css/login.css in the plugin folder.

/ Basic branded background and logo sizing /
body.login {
    background: #f5f7fb
    min-height: 100vh
}

.login h1 a {
    background-image: url(../images/custom-logo.png)
    background-size: contain
    width: 320px
    height: 80px
    display: block
}

/ Center the form with a subtle shadow /
.login #login {
    padding: 32px 32px 32px
    background: #ffffff
    box-shadow: 0 6px 18px rgba(0,0,0,0.08)
    border-radius: 6px
}

/ Custom submit button color /
.wp-core-ui .button-primary {
    background: #2b9af3
    border-color: #2287d3
    box-shadow: none
    text-shadow: none
}

/ Smaller forget password link /
#nav a, #backtoblog a {
    color: #666
    font-size: 13px
}

Sample JavaScript (assets/js/login.js)

This script intercepts the default login form submission, sends data via AJAX to admin-ajax.php and handles responses. Save it as assets/js/login.js. It uses the localized cleLogin object defined via wp_localize_script above.

(function () {
    use strict

    // Helper to display messages above the form
    function showMessage(msg, isError) {
        var container = document.getElementById(cle-message)
        if (!container) {
            container = document.createElement(div)
            container.id = cle-message
            var loginForm = document.getElementById(login)
            loginForm.parentNode.insertBefore(container, loginForm)
        }
        container.textContent = msg
        container.style.color = isError ? #b32d2e : #2b7a2b
        container.style.marginBottom = 12px
    }

    // Wait for DOM
    document.addEventListener(DOMContentLoaded, function () {
        var form = document.getElementById(loginform)
        if (!form  !window.cleLogin) {
            return
        }

        form.addEventListener(submit, function (e) {
            // Prevent standard submit to demonstrate AJAX login
            e.preventDefault()

            var username = form.querySelector(#user_login).value
            var password = form.querySelector(#user_pass).value
            var remember = form.querySelector(#rememberme).checked ? 1 : 0

            showMessage(Logging in..., false)

            fetch(cleLogin.ajax_url, {
                method: POST,
                credentials: same-origin,
                headers: {
                    Content-Type: application/x-www-form-urlencoded charset=UTF-8
                },
                body: new URLSearchParams({
                    action: cle_ajax_login,
                    security: cleLogin.nonce,
                    username: username,
                    password: password,
                    remember: remember
                })
            }).then(function (res) {
                return res.json()
            }).then(function (data) {
                if (data.success) {
                    showMessage(Login successful — redirecting..., false)
                    var redirect = data.data  data.data.redirect ? data.data.redirect : window.location.href
                    window.location.href = redirect
                } else {
                    var msg = (data.data  data.data.message) ? data.data.message : cleLogin.i18n.ajax_error
                    showMessage(msg, true)
                }
            }).catch(function (err) {
                showMessage(Network error:    err.message, true)
            })
        }, false)
    })
})()

Alternative: Enqueue inline CSS using wp_add_inline_style

Sometimes you may prefer a tiny amount of inline CSS that adapts dynamically (for example, a color chosen by a setting). You can register a small CSS file and then use wp_add_inline_style to add the dynamic content.

// Example inside cle_login_enqueue_assets (after registering cle-login-style)
dynamic_css = sprintf(
    body.login { background: %s },
    esc_attr( get_option( cle_login_bg, #f5f7fb ) )
)
wp_add_inline_style( cle-login-style, dynamic_css )

Adding content or notices above the form

Use the login_message filter to add HTML above the login form. Remember to escape output appropriately.

function cle_login_message( message ) {
    custom = 

. esc_html__( Welcome. Use your credentials to access the dashboard., custom-login ) .

return custom . message } add_filter( login_message, cle_login_message )

Security checklist

  • Nonces: Use check_ajax_referer on AJAX handlers. Create nonces with wp_create_nonce and pass via wp_localize_script.
  • Sanitization: Sanitize and validate all incoming data (sanitize_text_field, wp_unslash, etc.).
  • Server-side authentication: Always perform authentication server-side (wp_signon, wp_authenticate), never trust the client.
  • Limit leaks: Avoid exposing sensitive debugging messages to the client. Return minimal, localized error messages.
  • HTTPS: Ensure your site uses SSL/TLS so credentials are transmitted securely. Use is_ssl() when relevant.

Performance and compatibility tips

  • Only enqueue on login pages: Use login_enqueue_scripts so assets are not loaded sitewide.
  • Version static assets: Use filemtime to automatically bump versions during development.
  • Avoid heavy libraries: Prefer vanilla JS for simple enhancements. If you require jQuery, declare it as a dependency to avoid duplicate libraries.
  • Testing: Test on different screen sizes and with browser autofill. Some browsers treat login fields specially ensure JS still works when fields are pre-filled.

Advanced: Using the REST API instead of admin-ajax.php

You can implement login-related logic with a custom REST endpoint. This is modern and can be more performant, but requires careful handling because you are creating endpoints that operate for unauthenticated users.

// Register a REST route (in your plugin)
add_action( rest_api_init, function () {
    register_rest_route( cle/v1, /login, array(
        methods             => POST,
        callback            => cle_rest_login,
        permission_callback => __return_true, // allow unauthenticated
    ) )
} )

function cle_rest_login( WP_REST_Request request ) {
    params = request->get_json_params()
    nonce  = isset( params[nonce] ) ? params[nonce] : 

    // Optionally verify a nonce if you passed one from wp_localize_script
    if ( ! wp_verify_nonce( nonce, cle-login-nonce ) ) {
        return new WP_Error( invalid_nonce, Invalid nonce, array( status => 403 ) )
    }

    // Authenticate similar to the AJAX example...
}

Common troubleshooting

  • Script not loading: Confirm your plugin or theme file paths and that files are readable. Check the browser console for 404s or JS errors.
  • Nonce failing: Ensure you create the nonce on the server (wp_create_nonce) and pass the correct name to check_ajax_referer. Use consistent naming for the action and the nonce.
  • Login redirects to wp-login.php?redirect_to=/…: When using AJAX, return the intended redirect and call window.location.href on success. Alternatively, gracefully fall back to normal form submission.
  • Issues with caching: If your login page is cached by a plugin or server, make sure login pages and AJAX endpoints are excluded from caching.

Additional customizations you may want

  • Show/hide password toggle (JS)
  • reCAPTCHA integration (server-side verification required)
  • Custom form fields (meta or multi-tenant identifiers handle on server-side)
  • Multi-language messages using WordPress i18n functions (use __(), _e() and text domains in PHP and localized strings via wp_localize_script)

Final notes and best practices

Always keep the following in mind:

  • Do not modify core files. Use plugins or child themes.
  • Keep server-side verification authoritative. Client-side JS is only for user experience.
  • Version and cache-bust assets. Use filemtime or an explicit version number so updates propagate to users browsers.
  • Test with accessibility in mind. Ensure color contrast, keyboard navigation and ARIA attributes where relevant.
  • Keep it simple. Overly complex login pages increase the risk of bugs and support issues.

Useful references

Complete example recap

Use the plugin scaffold above as a starter: it demonstrates the recommended pattern. It registers and enqueues login-specific CSS and JS, localizes the script with an AJAX URL and a nonce, modifies the logo link and title attributes, and provides a secure AJAX login handler that uses wp_signon. Adapt the CSS and JS to your branding needs and extend the server-side logic to add logging, rate limiting, CAPTCHA verification, or other site-specific requirements.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *