WP Manifestindependent plugin directory
manifest / themes / isudev-wp-login-theme

IsuDev Login self-updates

WP Theme login simply no crazy stuff

by IsuDev · github.com/isudevelopment/isudev-wp-login-theme · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/isudevelopment/isudev-wp-login-theme/archive/refs/heads/main.zip

Ships its own WordPress updater (Plugin Update Checker), so new versions show up under Dashboard → Updates.

Custom wp-login.php screens — background image, logo and kicker text — styled with the active theme's theme.json tokens. Reusable across projects: nothing here is Stelmach-specific, and every value comes from settings or filters.

wp-login.php is never overridden and no core file is copied; all markup is added through core hooks.

Settings

Settings → Login screen (options-general.php?page=isudev-login), stored in one option, isudev_login.

Field Empty means
Background image No image panel — the form is centered on the screen
Image position left (default) or right
Logo Falls back to the site logo (custom_logo), then to the site name as text
Kicker text Hidden. Rendered on the background image, so it needs one
Heading Hidden. Shown on the login form only, not on lost/reset password
Intro text Hidden. Shown on the login form only

The small label above the heading is the localized screen title from core ("Log In", "Lost Password", …), so every screen gets one without configuration.

Layout

From 841px up the image panel is pinned to one side of the viewport and the form column scrolls next to it. Below that the panel becomes a max(34vh, 240px) band above the form. Without a background image there is one centered column at any width.

Styling

login_enqueue_scripts prints the theme's theme.json variables, presets and @font-face rules onto the login screen, then loads assets/login.css.

Every neutral is mixed in CSS from exactly two anchors — --isudev-login-base (--wp--preset--color--base, default #fff) and --isudev-login-contrast (--wp--preset--color--contrast, default #16181d) — so the palette follows whichever polarity the theme uses. A theme's own contrast-2, muted, surface-accent, border-strong and border-subtle presets are preferred when it has them.

That derivation is the point, not a detail: fixed fallbacks can land a light neutral on a light background as soon as one preset is missing, which is how 0.2.x ended up drawing white input rules on a white page under Twenty Twenty-Five. Mixing from the anchors makes that combination unreachable.

Tokens

Colour, in the order they derive. base and contrast are the two anchors; a percentage means color-mix(in srgb, contrast N%, base).

Token Default Used for
--isudev-login-base base, else #fff Anchor: the background
--isudev-login-contrast contrast, else #16181d Anchor: the text
--isudev-login-bg the base Page background
--isudev-login-surface surface-accent, else 6% Image panel behind a missing image, notices
--isudev-login-strong the contrast Headings, field text, focus rings
--isudev-login-text contrast-2, else 82% Body text, field labels, footer links
--isudev-login-muted muted, else 66% Intro text, remember-me, reveal-password icon
--isudev-login-border border-strong, else 52% Decorative rules: notices, language switcher
--isudev-login-border-subtle border-subtle, else 14% The hairline above #nav
--isudev-login-field-border always 52%, never the theme preset Input and checkbox boundaries
--isudev-login-field-bg 4% The tint that gives a field its extent
--isudev-login-accent accent, else the contrast colour Submit background, kicker dash, hover
--isudev-login-accent-text the accent Text painted in the accent — see Accessibility
--isudev-login-on-accent the base The submit label, painted on the accent
--isudev-login-danger danger, else #c6402f #login_error border
--isudev-login-link body text #nav, #backtoblog, privacy link — core repaints these with the admin colour scheme, so they need their own tokens
--isudev-login-link-hover accent text The same links on hover and focus

Everything else:

Token Default Used for
--isudev-login-display font-family--display, else --body Logo text, heading
--isudev-login-body font-family--body, else system-ui Everything else
--isudev-login-radius custom--radius--small, else 2px Submit, field top corners, buttons
--isudev-login-form-width 420px Form column, excluding the gutter
--isudev-login-gutter clamp(20px, 5vw, 64px) Padding outside the form column
--isudev-login-media-size 50vw Width of the pinned image panel from 841px up
--isudev-login-ease cubic-bezier(0.2, 0.7, 0.2, 1) Transitions

Markup to hook onto

The plugin's own elements, all added through core hooks:

Selector Where it comes from
.isudev-login__media Image panel, on login_header — a sibling of #login
.isudev-login__image / __scrim The attachment, and the gradient over it
.isudev-login__kicker Kicker text on the image
.isudev-login__header Wrapper above the form, on login_message
.isudev-login__label Localized screen title ("Log In", "Lost Password"), aria-hidden because core already prints it for screen readers
.isudev-login__heading / __intro Heading and intro settings, login screen only

Body classes, for branching without touching PHP: isudev-login, isudev-login--<action> (login, lostpassword, rp), and then either isudev-login--has-media with --media-left / --media-right, or isudev-login--centered; plus isudev-login--text-logo when no logo image resolved.

Input and checkbox boundaries never use the theme's border preset. Those presets are tuned to be near-invisible hairlines — Stelmach's border-strong is white at 22%, 1.9:1 on its own background — and WCAG 1.4.11 asks 3:1 of a control's boundary.

Overriding from a project

A project whose palette uses different slugs remaps the tokens in one place instead of rewriting selectors. Put the CSS in a theme file and enqueue it on login_enqueue_scripts with isudev-login as a dependency, so it lands after the plugin's own stylesheet:

add_action(
    'login_enqueue_scripts',
    static function (): void {
        if ( ! wp_style_is( 'isudev-login', 'registered' ) ) {
            return;
        }

        wp_add_inline_style(
            'isudev-login',
            'body.login{
                --isudev-login-base: var(--wp--preset--color--background);
                --isudev-login-accent: var(--wp--preset--color--primary);
            }'
        );
    },
    20
);

The guard matters: the handle only exists once the plugin has run, so a theme that loads without it would otherwise attach the CSS to nothing.

Setting --isudev-login-base and --isudev-login-contrast is usually the whole job — every neutral follows. Reach for a specific token only when one element needs to differ from what the anchors give it.

Accessibility

The measured pairs on the two palettes the plugin is developed against — Twenty Twenty-Five (#fff / #111) and Stelmach (#0B0B0D / #F4F2EE) — clear WCAG 2.2 AA: body text and labels 11:1, field text 16–17:1, submit label 9–19:1, footer links 11:1, field and checkbox boundaries 3.5–4.9:1 against 1.4.11's 3:1.

Two colours cannot be settled in CSS, because they depend on the accent's luminance and CSS cannot branch on it: the submit label sitting on the accent, and the kicker painted in it. includes/contrast.php computes both in PHP from the resolved palette and emits them as custom properties, and only when the CSS default would fail. Twenty Twenty-Four is the case that needs it — accent is #cfcabe on #f9f9f9, so the label goes to #111111 (1.2:1 → 11.6:1) and the kicker falls back to the body text colour. A palette it cannot read leaves the CSS defaults, which are derived and therefore already safe.

The rest is ordinary: a 2px focus ring on every control including the reveal-password button and the checkbox, a 44×44 target on that button, focus that does not move the control it lands on, footer links underlined rather than distinguished by colour alone, no hover state that lowers contrast, and a forced-colors block so Windows high-contrast mode keeps field boundaries and focus rings.

Filters

Filter Purpose
isudev_login_fields Add, remove or relabel settings fields. Types: image, text, textarea, select; a field may carry its own sanitize callback
isudev_login_settings Override resolved settings, e.g. hard-code them in a theme
isudev_login_image_id Background image attachment ID
isudev_login_logo_id Logo attachment ID (0 for the text logo)
isudev_login_generic_errors Return false to keep core's credential error messages

Wrong username or password otherwise collapses into one message, so the screen does not reveal whether an account exists.

Extra fields are read with IsuDev\Login\get_setting( 'key' ) and rendered from whichever core login hook fits — login_header (before #login), login_message (above the form) or login_footer.

Install in another project

Bedrock-style repo — add the VCS repository and require the package:

"isudev-login": {
    "type": "vcs",
    "url": "git@github.com:IsuDevelopment/isudev-wp-login-theme.git"
}
composer require isudev/login

Anywhere else, download isudev-login-theme.zip from the releases page and install it as a plugin. Packaged installs then update themselves from later releases.

Updates

Packaged installs check GitHub releases through Plugin Update Checker and install the release asset, which carries vendor/.

A checkout containing .git — the development symlink, or a Composer path install — never self-updates, so WordPress cannot overwrite a working tree. Sites managed by Composer or a deploy pipeline can also define ISUDEV_LOGIN_DISABLE_SELF_UPDATES or return false from isudev_login_self_updates_enabled.

Development

The plugin is developed in ~/Other Projects/isu-plugins/isudev-login-theme and symlinked into the isudev-library Local site at ~/Local Sites/isudev-library/app/public/wp-content/plugins/isudev-login-theme. See ../README.md.

composer install
composer check      # phpcs + phpstan

Releasing

  1. Bump the version in three places — the Version: header and the VERSION constant in plugin.php, and Stable tag: in readme.txt. The workflow fails the release if they disagree.
  2. Add a = x.y.z = section to the changelog in readme.txt. The release body is lifted from it, and a missing section fails the release.
  3. Commit, then git tag vX.Y.Z && git push origin main --tags.

.github/workflows/release.yml builds the ZIP with production dependencies only, asserts no development files leaked into it, and publishes the GitHub release.