Plain Custom Login

Header image for Plain Custom Login

A simple branded login page.

8 minute reading time of 1724 words (inc code) Code available at Plugin available at WordPress This code is marked as "old" - in other words, while it won't set fire to your kitchen, it's unsupported and hasn't been tested on any latest versions of anything, and so it probably won't work without heavy tweaking. Caveat emptor, here be dragons etc...

I don’t normally need anything too complicated to replace the default WP login screen, so I’d rather use this to rebrand it than one of the alternative plugins as they tend to have more bells and whistles than wanted.

// queue up our css on the relevant pages
add_action('login_init', array($this, 'register_styles_and_scripts'));
add_action('admin_init', array($this, 'register_styles_and_scripts'));
add_action('login_footer', array($this, 'enqueue_styles_and_scripts'));
add_action('admin_footer', array($this, 'enqueue_styles_and_scripts'));

// remove the default WP styling to avoid unnecessary conflicts
add_action('login_init', array($this, 'remove_head_links_start'));
add_action('login_head', array($this, 'remove_head_links_end'));

// and adjust the default HTML so we can pretty up the checkbox
add_action('login_form', array($this, 'alter_forget_me_html_start'), 10);
add_action('login_footer', array($this, 'alter_forget_me_html_end'), 10);

// replace the default WP titles with the site specific ones
add_filter('login_headerurl', array($this, 'custom_login_header_url'));
add_filter('login_headertitle', array($this, 'custom_login_header_title'));

// plugin settings
add_action('admin_init', array($this, 'settings_init'));
add_action('admin_menu', array($this, 'add_settings_page'));

The usual hooks in the plugin constructor, queuing up actions to take on the admin and login pages.

public function register_styles_and_scripts() {
    wp_register_style('plain-custom-login-style', plugins_url('style.css', __FILE__), false, '0.1');
    wp_register_script('plain-custom-login-script', plugins_url('admin.js', __FILE__),
        \ array('wp-color-picker'));

public function enqueue_styles_and_scripts() {
    if (is_admin()) {
        // only need the colour picker script in the admin
        wp_enqueue_script('plain-custom-login-script', plugins_url('admin.js', __FILE__),
            \ array('wp-color-picker'), false, true);

    } else {
        // if we're on the login page, we still need to get the options
        if (! ($this->options = get_option('PlainCustomLoginPluginOptions'))) {
            $this->options = $this->make_default_selections();

    $custom_css = '
        body.login {
        # etc etc etc
    wp_add_inline_style('plain-custom-login-style', $custom_css);

I normally keep the registration of the CSS and JS separate from the actual bit that writes out the relevant tags in the HTML, because the registration always needs to be done at the start but I usually don’t want any of my extra styling/scripting to be loaded until after everything else is done (ie. in the footer).

admin.js is a tiny script that associates the colour picker with the right form fields, and prevents the in-built WP colour picker widget from being loaded with an extra colour picker that doesn’t work.

The custom_css also references a bunch of standard colour functions I use to determine contrasting colours, or to convert RGB to/from HSL to get different shades more easily. This lets me write out text and box shadows into the CSS without asking the user for yet more palette choices.

public function remove_head_links_start() {
    ob_start(array($this, 'remove_head_links'));
public function remove_head_links($head) {
    return preg_replace("/<link rel='stylesheet' id='.+\-css'.+>/", '', $head);
public function remove_head_links_end() {

public function alter_forget_me_html_start() {
    ob_start(array($this, 'alter_forget_me_html'));
public function alter_forget_me_html($body) {
    return preg_replace(
        '|<p class="forgetmenot"><label for="rememberme">(<input.+>)\s*(.+)</label></p>|',
        '<p class="forgetmenot">\1 <label for="rememberme">\2?</label></p>',
public function alter_forget_me_html_end() {

I did originally use deregister_style but as WP still echoed the <link>, it kept conflicting with the new styling. This way removes all five of the separate WP CSS files that are included on the login page (and any other with a -css id).

And - in Firefox at least - the label for the “Remember Me” part of the login form didn’t work with the <input> tag inside the <label>, so I move it outside (and put a question mark on the end while I’m there).

public function custom_login_header_url($url) {
    return get_bloginfo('url'); // rather than
public function custom_login_header_title($message) {
    return get_bloginfo('description'); // rather than Powered by WP

Because who wants the WP logo link rather than their site name at the top of their login page?!

public function settings_init() {
    if (! ($this->options = get_option('PlainCustomLoginPluginOptions'))) {
        add_option('PlainCustomLoginPluginOptions', $this->make_default_selections(), null, false);

    register_setting('PlainCustomLoginPluginOptions', 'PlainCustomLoginPluginOptions',
        \ array($this, 'validate_settings'));
    add_settings_section('PlainCustomLoginSettings', __('Colours', 'PlainCustomLogin'),
        \ array($this, 'colour_settings_form'), 'PlainCustomLoginPlugin');

public function add_settings_page() {
    add_options_page(__('Plain Custom Login Page Options', 'PlainCustomLogin'), __('Plain Custom Login',
        \ 'PlainCustomLogin'), 'manage_options', __CLASS__.'/settings.php',
        \ array($this, 'display_settings_page'));

private function make_default_selections() {
    return array(
        'colours' => array(
            'base'     => array('name' => __('Background', 'PlainCustomLogin'), 'value' => '#b43c38'),
            'title'    => array('name' => __('Main Title', 'PlainCustomLogin'), 'value' => '#ffffff'),
            'msgback'  => array('name' => __('Message Background', 'PlainCustomLogin'), 'value' => '#c29ec4'),
            'msgtext'  => array('name' => __('Message Text', 'PlainCustomLogin'), 'value' => '#111111'),
            'errback'  => array('name' => __('Error Background', 'PlainCustomLogin'), 'value' => '#e99e9d'),
            'errtext'  => array('name' => __('Error Text', 'PlainCustomLogin'), 'value' => '#111111'),
            'formback' => array('name' => __('Form Background', 'PlainCustomLogin'), 'value' => '#ffffff'),
            'formtext' => array('name' => __('Form Text', 'PlainCustomLogin'), 'value' => '#555555'),
            'links'    => array('name' => __('Links', 'PlainCustomLogin'), 'value' => '#ffffff'),

And the standard way of registering the plugin settings hooks for the admin section, plus creating default options.

public function display_settings_page() {
    echo '<div class="wrap"><h2>'.__('Plain Custom Login Page Options', 'PlainCustomLogin').'</h2>'
        .'<form action="options.php" method="post">';
    echo '<table class="form-table"><tbody>';
    echo '</tbody></table>';
    echo '</form></div>';

public function colour_settings_form() {
    $output = '';

    foreach ($this->options['colours'] as $colour => $data) {
        $output.= '<tr><th scope="row">'.$data['name'].'</th><td><fieldset>'
            . '<input type="text" class="color-field" size="5" id="PlainCustomLoginPluginOptions[colours]['.$colour.']"'
            . ' name="PlainCustomLoginPluginOptions[colours]['.$colour.']" value="'.$data['value'].'">'
            . '</fieldset></td></tr>';

    echo $output;

Only one setting section (the form list of colour options) was registered in the settings_init(), but if you wanted to expand the plugin to load in custom logos etc, here’s where it would go.

public function validate_settings($input) {
    $this->options = $this->make_default_selections();

    if (isset($input['colours']) AND is_array($input['colours'])) {
        foreach ($input['colours'] as $colour => $value) {
            if ($this->is_valid_colour($value)) $this->options['colours'][$colour]['value'] = $value;

    return $this->options;

private function is_valid_colour($value) {
    return preg_match('/^#(?:[0-9a-f]{3}){1,2}$/i', $value);

Finally, when the plugin options are saved, I load up the defaults again and only change each colour option if it passes validation.

Reply via email