IsuDev Login self-updates
WP Theme login simply no crazy stuff
by IsuDev · github.com/isudevelopment/isudev-wp-login-theme · website
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.zipShips 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
- Bump the version in three places — the
Version:header and theVERSIONconstant inplugin.php, andStable tag:inreadme.txt. The workflow fails the release if they disagree. - Add a
= x.y.z =section to the changelog inreadme.txt. The release body is lifted from it, and a missing section fails the release. - 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.