Noteworthy Admin CSS changes in WordPress 5.3

WordPress 5.3 will introduce a number of CSS changes in WordPress admin. While the necessity to improve wp-admin accessibility was previously raised in several Trac tickets, Gutenberg’s recent interface improvements made it necessary to improve the whole interface as well.

Background: in April 2019, WP-Campus conducted an accessibility audit of the new editor interface, made by an independent contractor, Tenon LLC. This audit raised issues in the editor but also in the media modal, which uses wp-admin styles. Fixing these issues on Gutenberg and on the media modal but not in the whole wp-admin interface would have been very inconsistent.

Some tickets were milestoned to the 5.3 release cycle to start backporting Gutenberg accessibility improvements to the whole admin interface. These first tickets aim to improve:

  • Color contrasts on form fields and buttons
  • Focus styles on form fields and buttons
  • Content behavior on text zoom

Backporting some of Gutenberg’s styles to fix these issues introduced some visual issues with the interface elements hierarchy. Therefore, Design and Accessibility teams worked on the overall visual hierarchy:

  • darker tables and metaboxes borders were introduced for a better hierarchy between interface elements

Note for plugin authors and WordPress developers

These changes are only CSS changes, and not structural changes, so the HTML markup is exactly the same as before, with the same class attributes on each element.

In short, your styles should align with these changes if interface elements are not overridden by custom CSS. If you are overriding WordPress Admin CSS on form elements, you should test your plugins or your custom developments against WordPress 5.3 RC 1.

If you are a plugin author, there are different use cases:

  • Plugins that are using default Admin CSS styles should work just like before.
  • Plugins that are using custom Admin CSS styles by overriding default Admin CSS should be checked against 5.3.
  • Plugins that are using fully customized Admin CSS styles should not be concerned by those changes.

In general, plugin authors and WordPress developers are encouraged to:

  • remove any fixed heights: flexible heights are the WordPress recommended standard (and one of the main goals of the Admin CSS changes).
  • remove any custom top and bottom padding values.
  • remove any custom line-height values.
  • update their CSS code to override new focus/hover buttons colors if they use custom colors on this type of element.

In the next section of this dev note, you’ll find some noteworthy CSS changes coming in WordPress 5.3.

Main things that are changing in 5.3:

  • Forms fields: 
    • text inputs
    • textareas
    • selects
    • checkboxes
    • radiobuttons
    • both primary and secondary buttons
    • colorpickers
  • Tables, notifications and metaboxes

Known issues

Available for testing in WordPress 5.3 RC 1, these changes have been tested in various use cases and no breakage situation was identified during the tests. Please check the report for full information about the testing panel.

This is a work in progress, just like anything in WordPress Core. These usability improvements were implemented during summer 2019 then tested and iterated on September and October. After 5.3 is released, the idea is to iterate on wp-admin design to make it fully consistent with the editor interface, and to provide a great and accessible editorial experience for websites administrators. The next minor releases will fix small issues with 5.3 changes and the next majors will improve the consistency of user experiences between Gutenberg and WordPress administration.

Changes related to form fields

Text inputs

Legacy CSS code:

/* Legacy styles */
input[type=text] {
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
input[type=text]:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;
}
Legacy input style
Legacy focused input style

New CSS code:

/* New styles */
input[type=text] {
    padding: 0 8px;
    line-height: 2;
    min-height: 30px;
    box-shadow: 0 0 0 transparent;
    border-radius: 4px;
    border: 1px solid #7e8993;
    background-color: #fff;
    color: #32373c;
}
input[type=text]:focus {
    border-color: #007cba;
    box-shadow: 0 0 0 1px #007cba;
    outline: 2px solid transparent;
}
New input style
New focused input style

Textareas

Legacy CSS code:

/* Legacy styles */
textarea {
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
textarea:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;
}
Legacy textarea style
Legacy focused textarea style

New CSS code:

/* New styles */
textarea {
    box-shadow: 0 0 0 transparent;
    border-radius: 4px;
    border: 1px solid #7e8993;
    background-color: #fff;
    color: #32373c;
}
textarea:focus {
    border-color: #007cba;
    box-shadow: 0 0 0 1px #007cba;
    outline: 2px solid transparent;
}
New textarea style
New focused textarea style

Selects

Legacy CSS code:

/* Legacy styles */
select {
    padding: 2px;
    line-height: 28px;
    height: 28px;
    vertical-align: middle;
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
select:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;    
}
Legacy select style
Legacy focused select style

New CSS code:

select {
    font-size: 14px;
    line-height: 1.28571428;
    color: #32373c;
    border: 1px solid #7e8993;
    border-radius: 3px;
    box-shadow: none;
    padding: 4px 24px 4px 8px;
    min-height: 30px;
    max-width: 25rem;
    vertical-align: middle;
    -webkit-appearance: none;
    background: #fff url(data:image/svg+xml;charset=US-ASCII,<svg image>) no-repeat right 5px top 55%;
    background-size: 16px 16px;
    cursor: pointer;
}
select:focus {
    border-color: #007cba;
    color: #016087;
    box-shadow: 0 0 0 1px #007cba;
}
New select style
New focused select style

Radiobuttons

Legacy CSS code:

/* Legacy styles */
input[type=radio] {
    border: 1px solid #b4b9be;
    border-radius: 50%;
    background: #fff;
    color: #555;
    clear: none;
    cursor: pointer;
    display: inline-block;
    line-height: 0;
    height: 16px;
    margin: -4px 4px 0 0;
    outline: 0;
    padding: 0!important;
    text-align: center;
    vertical-align: middle;
    width: 16px;
    min-width: 16px;
    -webkit-appearance: none;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
    transition: .05s border-color ease-in-out;
}
input[type=radio]:checked:before {
    content: "\2022";
    text-indent: -9999px;
    border-radius: 50px;
    font-size: 24px;
    width: 6px;
    height: 6px;
    margin: 4px;
    line-height: 16px;
    background-color: #1e8cbe;
}
Legacy radio buttons styles

New CSS code:

/* New styles */
input[type=radio] {
	border: 1px solid #7e8993;
	border-radius: 50%;
	line-height: .71428571;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	height: 1rem;
	margin: -.25rem .25rem 0 0;
	outline: 0;
	padding: 0 !important;
	text-align: center;
	vertical-align: middle;
	width: 1rem;
	min-width: 1rem;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=radio]:checked::before {
	content: "";
	border-radius: 50%;
	width: .5rem;
	height: .5rem;
	margin: .1875rem;
	background-color: #1e8cbe;
	line-height: 1.14285714;
}
New radio buttons styles

Checkboxes

Legacy CSS code:

/* Legacy styles */
input[type=checkbox] {
	border: 1px solid #b4b9be;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	line-height: 0;
	height: 16px;
	margin: -4px 4px 0 0;
	outline: 0;
	padding: 0!important;
	text-align: center;
	vertical-align: middle;
	width: 16px;
	min-width: 16px;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=checkbox]:checked:before {
	content: "\f147";
	margin: -3px 0 0 -4px;
	color: #1e8cbe;
}
input[type=checkbox]:focus {
	border-color: #5b9dd9;
	box-shadow: 0 0 2px rgba(30,140,190,.8);
	outline: 2px solid transparent;
}
Legacy checkbox styles
Legacy focused checkbox style

New CSS code:

/* New styles */
input[type=checkbox], input[type=radio] {
	border: 1px solid #7e8993;
	border-radius: 4px;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	line-height: 0;
	height: 1rem;
	margin: -.25rem .25rem 0 0;
	outline: 0;
	padding: 0!important;
	text-align: center;
	vertical-align: middle;
	width: 1rem;
	min-width: 1rem;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=checkbox]:checked::before {
	content: url(data:image/svg+xml;utf8,<svg image>);
	margin: -.1875rem 0 0 -.25rem;
	height: 1.3125rem;
	width: 1.3125rem;
}
input[type=checkbox]:focus {
	border-color: #007cba;
	box-shadow: 0 0 0 1px #007cba;
	outline: 2px solid transparent;
}
New checkbox style
New focused checkbox style

Buttons

Legacy CSS code:

/* 
 * Legacy styles for buttons
 */

/* General */
.wp-core-ui .button, .wp-core-ui .button-primary, .wp-core-ui .button-secondary {
	display: inline-block;
	text-decoration: none;
	font-size: 13px;
	line-height: 26px;
	height: 28px;
	margin: 0;
	padding: 0 10px 1px;
	cursor: pointer;
	border-width: 1px;
	border-style: solid;
	-webkit-appearance: none;
	border-radius: 3px;
	white-space: nowrap;
	box-sizing: border-box;
}

/* Primary buttons styles */
.wp-core-ui .button-primary {
	background: #0085ba;
	border-color: #0073aa #006799 #006799;
	box-shadow: 0 1px 0 #006799;
	color: #fff;
	text-decoration: none;
	text-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799;
}
/* Primary buttons :hover styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #008ec2;
	border-color: #006799;
	color: #fff;
}
/* Primary buttons :focus styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary:focus {
	box-shadow: 0 1px 0 #0073aa, 0 0 2px 1px #33b3db;
}

/* Secondary buttons styles */
.wp-core-ui .button, .wp-core-ui .button-secondary {
	color: #555;
	border-color: #ccc;
	background: #f7f7f7;
	box-shadow: 0 1px 0 #ccc;
	vertical-align: top;
}
/* Secondary buttons :hover styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button-secondary:hover, .wp-core-ui .button.focus, .wp-core-ui .button.hover, .wp-core-ui .button:focus, .wp-core-ui .button:hover {
	background: #fafafa;
	border-color: #999;
	color: #23282d;
}

/* Secondary buttons :focus styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button.focus, .wp-core-ui .button:focus {
	border-color: #5b9dd9;
	box-shadow: 0 0 3px rgba(0,115,170,.8);
}
Legacy style for primary button
Legacy style for focused primary button
Legacy style for secondary button
Legacy style for focused secondary button

New CSS code:

/* 
 * New styles for buttons
 */

/* General */
.wp-core-ui .button, .wp-core-ui .button-primary, .wp-core-ui .button-secondary {
	display: inline-block;
	text-decoration: none;
	font-size: 13px;
	line-height: 2.15384615;
	min-height: 30px;
	margin: 0;
	padding: 0 10px;
	cursor: pointer;
	border-width: 1px;
	border-style: solid;
	-webkit-appearance: none;
	border-radius: 3px;
	white-space: nowrap;
	box-sizing: border-box;
}

/* Primary buttons styles */
.wp-core-ui .button-primary {
	background: #007cba;
	border-color: #007cba;
	color: #fff;
	text-decoration: none;
	text-shadow: none;
}
/* Primary buttons :hover styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #0071a1;
	border-color: #0071a1;
	color: #fff;
}
/* Primary buttons :focus styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #0071a1;
	border-color: #0071a1;
	color: #fff;
}

/* Secondary buttons styles */
.wp-core-ui .button, .wp-core-ui .button-secondary {
	color: #0071a1;
	border-color: #0071a1;
	background: #f3f5f6;
	vertical-align: top;
}
/* Secondary buttons :hover styles */
.wp-core-ui .button-secondary:hover, .wp-core-ui .button.hover, .wp-core-ui .button:hover {
	background: #f1f1f1;
	border-color: #016087;
	color: #016087;
}
/* Secondary buttons :focus styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button.focus, .wp-core-ui .button:focus {
	background: #f3f5f6;
	border-color: #007cba;
	color: #016087;
	box-shadow: 0 0 0 1px #007cba;
	outline: 2px solid transparent;
	outline-offset: 0;
}
New style for primary button
New style for focused primary button
New style for secondary button
Legacy style for focused secondary button

Darker borders on tables, notices, metaboxes and other similar elements

These changes introduce better contrast for borders for the following user interface elements:

  • Tables
  • Screen Options and Help
  • Admin notices
  • Welcome panel
  • Meta boxes (post boxes on classic editor or in edit attachment screens)
  • Cards
  • Health Check accordions and headings
  • Theme and Plugin upload forms

Legacy CSS code:

Depending on the related element, several CSS declarations were used.

border: 1px solid #e5e5e5;
border: 1px solid #e1e1e1;
border: 1px solid #eee;
border: 1px solid #ddd;
border: 1px solid #e2e4e7;
Legacy card with light grey border
Legacy table with light grey border
Legacy Screen options panel with light grey border
Legacy notice with light grey border

New CSS code:

All those CSS declaration are replaced with a unique color:

border: 1px solid #ccd0d4;
New card with darker border
New table with darker border
Legacy Screen options panel with darker border
New notice with darker border

Related Trac tickets

  • #47153: Field boundaries have insufficient color contrast
  • #34904: The design of the focus outline on buttons/elements could be improved
  • #47477: Content overflows and is cut off at 200% text enlarge
  • #47498: Revise checkbox/radio button css for better compatibility with text zoom
  • #48101: Use darker table + card borders across WP-Admin

#5-3, #accessibility, #dev-notes

WordPress 5.3: Site Admin Email Verification Screen

In WordPress 5.3, a new screen has been introduced to help ensure the site’s administration email remains accurate and up to date. The site’s admin email (as defined when installing WordPress, and found on the Settings > General page) is a critical part of every WordPress site. This new screen will help site owners remain in full control of their site, even as years go by.

How does this work?

By default, administrators will see a screen after logging in that lists the site’s admin email address once every 6 months.

They are presented with 4 actions:

  1. The site’s email is verified as correct: After clicking “The email is correct” button, the user is taken to the Dashboard with an admin notice saying “Thank you for verifying”. The screen will be hidden for 6 months from all administrators.
  2. The site’s email needs to be changed: After clicking the “Update” button, the user is taken to the Settings > General page where they can update the site’s email address. Administrators will be presented with the verification screen the next time they log in.
  3. The user clicks “Remind me later”: the user is taken to the Dashboard. Administators will see the screen again after 3 days have passed.
  4. Back to “Site Name”: When this link is clicked, the user will be taken to the site’s home page. Administrators will be presented with the verification screen the next time they log in.

Available Actions and Filters

Actions

There are a few action hooks on the verification page that developers can use to customize the screen.

  • admin_email_confirm – Fires before the admin email confirm form.
  • admin_email_confirm_form – Fires inside the admin-email-confirm-form form tag, but before any other output.

A new action hook after the form was not introduced. Instead, use the login_footer action, which is called just after the closing </form> tag. The if ( 'confirm_admin_email' === $_GET['action'] ) conditional can be used to check that the email verification screen is being shown.

Filters

One new filter, admin_email_check_interval, has also been introduced. This filter can be used to change the frequency that administrators should see the verification screen.

The following example changes the interval from the default of 6 months to 2 months:

<?php
function myplugin_admin_email_check_interval( $interval ) {
	return 2 * MONTH_IN_SECONDS;
}
add_filter( 'admin_email_check_interval', 'myplugin_admin_email_check_interval' );

Note: The returned value should always be in seconds.

This filter can also be used to disable the feature by returning a “falsey” value, such as 0, or false.

The following example will disable the admin email verification check:

<?php
add_filter( 'admin_email_check_interval', '__return_false' );

For more information about these changes, check out #46349 in Trac.

Props to @desrosj for peer review.

#5-3, #admin, #dev-notes

The REST API in WordPress 5.3

WordPress 5.3 contains a number of REST API improvements designed to make it easier and faster to work with API data from the block editor or other client applications.

Register Array & Object Metadata

As covered previously in this developer note on array & object metadata, it is now possible to use register_post_meta & register_term_meta (as well as the underlying method register_meta) to interact with complex meta values as schema-validated JSON arrays or objects using the REST API. See the linked post for more details.

Nested response filtering with _fields query parameter

This developer note on the changes to the REST API’s _fields= query parameter shows how you may now filter your REST API response objects to include only specific nested properties within the response body.

Ticket #42094

Set drafts back to “floating date” status

Once a date has been set for a draft, it was previously impossible to set the post back to showing “publish immediately” (also referred to as a “floating date,” where the post will be dated whenever it is published). As of 5.3, passing null for a date value will unset the draft date and restore this floating state.

Ticket #39953

Faster Responses

We have introduced a caching wrapper around the generation of REST resource schema objects, which initial testing has shown to yield up to a 30-40% performance increase in large API responses. If you work with expensive or large REST API queries, things should be quite a bit faster now. (Ticket #47871)

The REST API has also been improved to avoid unnecessary controller object instantiation (#45677) and to skip generation of sample permalinks when that data is not requested (#45605).

Please Note: if your team has existing performance benchmarking tooling for the REST API, please contact the component maintainers in the #core-restapi Slack channel; we very much desire to expand our metrics in this area.

Additional Changes of Note

In addition to these key enhancements, there are a number of smaller improvements to the REST API which may be of interest to developers.

  • It is no longer possible to DELETE a Revision resource using the REST API, as this behavior could break a post’s audit trail. Ticket #43709
  • The /search endpoint will now correctly embed the full original body of each matched resource when passing the _embed query parameter. Ticket #47684
  • rest_do_request and rest_ensure_request now accept a string API path, so it is possible to instantiate a request in PHP using nothing more than the desired endpoint string, e.g.
    rest_do_request( '/wp/v2/posts' );
    Ticket #40614
  • Creating or updating a Terms resource via the REST API now returns the updated object using the “edit” context. Ticket #41411
  • It is now possible to edit a posted comment through the REST API when authenticating the request as a user with the moderate_comments capability. Previously a full editor- or admin-level role was needed. Ticket #47024
  • rest_get_avatar_urls now receives the entire User or Comment object, not just the object’s email address. Ticket #40030

Welcoming Timothy Jacobs as a REST API component maintainer

Last but not least, many of you have no doubt seen @timothyblynjacobs active in trac, slack, and community events. Timothy has driven much of the momentum that resulted in the above improvements, and I’m excited to (belatedly) announce that he has joined the REST API team as an official component maintainer. Thank you very much for your energy and dedication!

Thank you also to every other person who contributed to API changes this cycle; it’s the best version of the REST API yet, and we couldn’t have done it without the dozens of contributors who helped create, review and land these patches.

We’ve got some ambitious ideas about how we can make the REST API even better in 5.4. Interested in helping out, with code, docs, or triage? Join us for weekly office hours, every week on Thursdays at 1800 UTC!

#5-3, #dev-notes, #rest-api

PHP Native JSON Extension Now Required

The PHP native JSON extension has been bundled and compiled with PHP by default since 5.2.0 (2006). However, a significant number of PHP installs did not include it. In order to ensure a consistent experience for JSON related functionality in all supported versions of PHP, WordPress Core has historically included a large number of workarounds, functions, and polyfills.

In 2011 (WordPress 3.2), an attempt was made to remove JSON related compatibility code. However, it was discovered that a fair number of distributions were still missing the PHP JSON extension by default, and the removed code was restored to ensure compatibility.

In WordPress 5.2, the minimum version of PHP supported was raised from 5.2.6 to 5.6.20. In the 8 year period since the last attempt was made to encourage use of the PHP native JSON extension, the number of distributions with this extension disabled has significantly decreased.

Because of this, the PHP native JSON extension is now required to run WordPress 5.3 and higher.

To prevent compatibility issues, a site that does not have the PHP native JSON extension enabled will see an error message when attempting to upgrade to WordPress 5.3. The update will be cancelled and the site will remain on the current version (see [46455]). This is to prevent potential compatibility issues on servers running custom PHP configurations.

Here’s a summary of what has changed.

Deprecated

The following functions and classes will remain in the code base, but will trigger a deprecated warning when used (see [46205]):

  • The Services_JSON and Services_JSON_Error classes and all methods
  • The wp-includes/class-json.php file
  • The (private) _wp_json_prepare_data() function

Removed

The following functions, and classes have been removed entirely from the code base (see the [46208] changeset):

  • json_encode() function polyfill
  • json_decode() function polyfill
  • _json_decode_object_helper() function polyfill
  • json_last_error_msg() polyfill
  • JsonSerializable interface polyfill
  • $wp_json global variable
  • JSON_PRETTY_PRINT constant polyfill
  • JSON_ERROR_NONE constant polyfill

Unchanged

The wp_json_encode() function will remain with no intention to deprecate it at this time. This function includes an extra sanity check for JSON encoding data and should still be used as the preferred way to encode data into JSON.

For more information about these changes, check out #47699 on Trac and the relevant changesets ([46205], [46206], [46208], [46377], and [46455]).

Props @jrf & @jorbin for peer review.

#5-3, #dev-notes, #php

Enhancements to the Network Sites Screen in WordPress 5.3

Changes to the database

The introduction of Site metadata in WordPress 5.1 has opened up a lot of new possibilities for multisite.

Save database version and date updated in multisite site meta

In [46193], the database version and the updated dates are now stored in the blogmeta table.

If your setup of multisite requires the database version to be accessed from a global context, instead of looping around every site with an expensive switch_to_blog call to get_option( 'db_version' ), you maybe want to try a function like the following.

function get_site_versions() {
	global $wpdb;
	$query = $wpdb->prepare( "SELECT blog_id, meta_value FROM $wpdb->blogmeta WHERE meta_key = 'db_version' ORDER BY blog_id DESC");
	return $wpdb->get_results( $query );
}

Remove blog_versions table

Currently, there is a table in multisite called blog_versions. This table stores the database version as a number and the updated date. It was introduced in #11644 and has never been used in Core since then.

With the database version and updated date now stored in theblogmeta table, blog_versionstable becomes redundant. In [46194], this table is removed from Core.

Changes to WP_MS_Sites_List_Table

WordPress 5.3 adds several enhancements to the WP_MS_Sites_List_Table class that allows plugin authors to take advantage of Site metadata to provide a richer experience for multisite administrators on the Network Admin Sites screen.

These enhancements will be very familiar to those who have used and/or customized the All Posts screen.

Site Status Views

The Network Sites screen now displays a list of links with the counts of Sites by status (e.g., Public, Spam, etc.), similar to the post status links on the All Posts screen.

Network Sites screen showing site status views.

The status links can also be filtered with the new views_sites-network filter, introduced in Trac ticket #37392.

For example, imagine there is a multisite where the main site acts as a directory of local restaurants and each separate site is for an individual restaurant, and restaurant owners can purchase a “subscription” that would allow them to display more information about their restaurant listing: a basic subscription would allow them to add photographs of their restaurant and an advanced subscription would additionally allow them to include their menu.

The subscription level could then be stored in the blogmeta table and “status” links can be added for the different subscription levels as follows:

add_filter( 'views_sites-network', 'myplugin_add_site_status_views' );
function myplugin_add_site_status_views( $view_links ) {
	$statuses = array(
		'free'      => _n_noop(
			'Free <span class="count">(%s)</span>',
			'Free <span class="count">(%s)</span>',
			'myplugin'
		),
		'basic'   => _n_noop(
			'Basic <span class="count">(%s)</span>',
			'Basic <span class="count">(%s)</span>',
			'myplugin'
		),
		'advanced'   => _n_noop(
			'Advanced <span class="count">(%s)</span>',
			'Advanced <span class="count">(%s)</span>',
			'myplugin'
		),
	);

	// get the count of sites with each of our custom statuses.
	$args = array(
		'meta_query' => array(
			array(
				'key'     => 'myplugin-status',
				'compare' => '=',
			),
		),
		'count' => true,
	);
	$counts = array();
	foreach ( array_keys( $statuses ) as $status ) {
		$args['meta_query'][0]['value'] = $status;
		$counts[ $status ] = get_sites( $args );
	}

	$requested_status = isset( $_GET['status'] ) ? wp_unslash( trim( $_GET['status'] ) ) : '';

	foreach ( $statuses as $status => $label_count ) {
		$current_link_attributes = $requested_status === $status ?
			' class="current" aria-current="page"' :
			'';
		if ( (int) $counts[ $status ] > 0 ) {
			$label = sprintf( translate_nooped_plural( $label_count, $counts[ $status ] ), number_format_i18n( $counts[ $status ] ) );

			$view_links[ $status ] = sprintf(
				'<a href="%1$s"%2$s>%3$s</a>',
				esc_url( add_query_arg( 'status', $status, 'sites.php' ) ),
				$current_link_attributes,
				$label
			);
		}
	}

	return $view_links;
}

When a user clicks on one of the custom status links, the rows in the list table can be limited to those sites with that specific custom Status using the existing ms_sites_list_table_query_args as follows:

add_filter( 'ms_sites_list_table_query_args', 'myplugin_sites_with_custom_status' );
function myplugin_sites_with_custom_status( $args ) {
	$status = ! empty( $_GET['status' ] ) ? wp_unslash( $_GET['status' ] ) : '';

	if ( empty( $status ) || ! in_array( $_GET['status'], array( 'free', 'basic', 'advanced' ) ) ) {
		return $args;
	}

	$meta_query = array(
		'key'   => 'myplugin-status',
		'value' => $status,
	);

	if ( isset( $args['meta_query'] ) ) {
		// add our meta query to the existing one(s).
		$args['meta_query'] = array(
			'relation' => 'AND',
			$meta_query,
			array( $args['meta_query'] ),
		);
	}
	else {
		// add our meta query.
		$args['meta_query'] = array(
			$meta_query,
		);
	}

	return $args;
}

Extra Tablenav

The posts displayed on the All Posts screen can be filtered by date and category. Plugins can also add custom filter criteria with the restrict_manage_posts filter.

To continue with the above restaurant guide example, imagine the food served by each restaurant is also stored in the blogmeta table. We could then allow Network administrators to filter the Sites by the type of food by adding a dropdown of the various cuisines:

Network Sites screen that includes a dropdown added with the "restrict_manage_sites" action.

Dropdowns like this can now be added on the Network Sites screen with the new restrict_manage_sites action ( introduced in Trac ticket #45954), as follows:

add_action( 'restrict_manage_sites', 'myplugin_add_cuisines_dropdown' );
function myplugin_add_cuisines_dropdown( $which ) {
	if ( 'top' !== $which ) {
		return;
	}

	echo '<select name="cuisine">';
	printf( '<option value="">%s</option>', __( 'All cuisines', 'myplugin' ) );

	$cuisines = array(
		'French'  => __( 'French', 'myplugin' ),
		'Indian'  => __( 'Indian', 'myplugin' ),
		'Mexican' => __( 'Mexican', 'myplugin' ),
	);
	
	$requested_cuisine = isset( $_GET['cuisine'] ) ? wp_unslash( $_GET['cuisine'] ) : '';
	
	foreach ( $cuisines as $cuisine => $label ) {
		$selected = selected( $cuisine, $requested_cuisine, false );
		printf( '<option%s>%s</option>', $selected, $label );
	}

	echo '</select>';

	return;
}

When a user selects a food type and clicks the Filter button, the rows in the list table can be limited to just those Sites that serve that cuisine using the existing ms_sites_list_table_query_args filter, as follows:

add_filter( 'ms_sites_list_table_query_args', 'myplugin_sites_with_cuisine' );
function myplugin_sites_with_cuisine( $args ) {
	if ( empty( $_GET['cuisine' ] ) ) {
		return $args;
	}

	$meta_query = array(
		'key'   => 'myplugin-cuisine',
		'value' => wp_unslash( $_GET['cuisine' ] ),
	);

	if ( isset( $args['meta_query'] ) ) {
		// add our meta query to the existing one(s).
		$args['meta_query'] = array(
			'relation' => 'AND',
			$meta_query,
			array( $args['meta_query'] ),
		);
	}
	else {
		// add our meta query.
		$args['meta_query'] = array(
			$meta_query,
		);
	}

	return $args;
}

Site Display States

As with other list tables, each row in the Sites list table can now have display states. By default, all Site statuses (other than Public) of each Site are included as display states. Additionally, the main Site for the Network also has the “Main” display state.

Network Sites screen showing display states added with the "display_site_states" filter.

When a specific Site status view has been selected by the user, that status will not be among the display states (this is just like the All Posts screen).

Plugins can also modify the display states with the new display_site_states filter, introduced in Trac ticket #37684.

To further continue our restaurant guide example, we can add our custom statuses and the cuisine served at each restaurant as display states. This can be achieved as follows:

add_filter( 'display_site_states', 'site_display_states', 10, 2 );
function site_display_states( $display_states, $site ) {
	$status = get_site_meta( $site->blog_id, 'myplugin-status', true );
	$requested_status = isset( $_GET['status'] ) ? wp_unslash( trim( $_GET['status'] ) ) : '';

	if ( $status !== $requested_status ) {
		switch ( $status ) {
			case 'free':
				$display_states['free']     = __( 'Free', 'myplugin' );
				break;
			case 'basic':
				$display_states['basic']    = __( 'Basic', 'myplugin' );
				break;
			case 'advanced':
				$display_states['advanced'] = __( 'Advanced', 'myplugin' );
				break;
		}
	}

	$cuisine = get_site_meta( $site->blog_id, 'myplugin-cuisine', true );
	$requested_cuisine = isset( $_GET['cuisine'] ) ? wp_unslash( trim( $_GET['cuisine'] ) ) : '';

	if ( $cuisine !== $requested_cuisine ) {
		$display_states[ $cuisine ] = $cuisine;
	}

	return $display_states;
}

Misc changes

Return for short circuits for multisite classes.

Fixing a bug created in the [44983] original patch, introduced pre query filters in multisite classes. This bug made short circuit act differently from other short circuits and as it still continues to execute. Now after the filter networks_pre_query and sites_pre_query run, the code will exit straight away. This allows developers to completely hot-wire the network and site query, to load from another source, such as a different cache or Elastic search.

Improved performance for site and network lookups by ID

In earlier versions of WordPress when running the code get_site( 12345 ) was run and no ID with that site exists, the result is not being cached. That means every subsequent lookup will still cause a DB query to be fired, which is unnecessary. In [45910] non-existent sites data is stored as -1 instead of false to save further database lookups.

#5-3, #dev-notes, #multisite, #networks-sites

Expanded meta key comparison operators in 5.3

WordPress 5.1 introduced the compare_key parameter for WP_Meta_Query, allowing developers to perform LIKE queries against postmeta keys. (See #42409 and the related dev note.) WordPress 5.3 expands the options available to compare_key, so that developers have access to meta-key comparison operators similar to those available for meta values. See #43446 and [46188].

After this change, compare_key accepts the following operators: =, LIKE, !=, IN,NOT IN,NOT LIKE,RLIKE,REGEXP,NOT REGEXP,EXISTS, andNOT EXISTS. Usage notes:

  • For parity with value operators, we’ve added support for EXISTS and NOT EXISTS. In the case of compare_key, these map to = and !=, respectively.
  • MySQL regular expression comparison operators (RLIKE, REGEXP, NOT REGEXP) are case-insensitive by default. To perform case-sensitive regular expression matches, it’s necessary to cast to BINARY. To support this, WP_Meta_Query now accepts a type_key parameter; pass 'BINARY' to force case-sensitive comparisons. (This directly parallels the use of type when using regular expressions to match meta value.)
  • As is the case with other WP_Meta_Query-related parameters, the parameters discussed here are available to WP_Query using the meta_ prefix: meta_compare_key and meta_type_key.

#5-3, #dev-notes, #query

Use aria-label to ensure Posts and Comments navigation has proper context in WordPress 5.3

Many themes (including bundled themes) use previous/next navigation in single post and pagination links in the posts/comments archives.

The markup output is under the responsibility of core private function _navigation_markup, which prints out an unlabelled <nav> element:

<nav class="navigation post-navigation" role="navigation">

This <nav> element defines an ARIA landmark by default: landmarks help assistive technology users to perceive the page main sections and jump through them. However, when a landmark is used more than once in a page, it needs to be distinguished from the other ones to let users understand what the landmark is about.

For reference, see ARIA Landmarks Example on W3.org.

To distinguish each context WordPress 5.3 will programmatically add specific aria-label parameter for each navigation menu. That parameter is being added to navigation menus generated by the following functions:

  • _navigation_markup()
  • get_the_post_navigation()
  • get_the_posts_navigation()
  • get_the_posts_pagination()
  • get_the_comments_navigation()
  • get_the_comments_pagination()

The following functions are also impacted as they are used to echo the result of the functions listed above:

  • the_post_navigation()
  • the_posts_navigation()
  • the_posts_pagination()
  • the_comments_navigation()
  • the_comments_pagination()

All these functions now implement aria_label parameter which can be used to pass custom value for the related HTML attribute.

For example:

the_post_navigation( array(
    'prev_text'          => __( 'previous: %title', 'text-domain' ),
    'next_text'          => __( 'next: %title', 'text-domain' ),
    'taxonomy'           => 'chapters',
    'aria_label'         => __( 'Chapters', 'text-domain' ),
) );

For reference, see the related Trac ticket: #47123

#5-3, #accessibility, #dev-notes

Changes to wp_die() HTML output in WordPress 5.3

By default and before WordPress 5.3, the handler for wp_die() wraps error messages with a paragraph tag.

For a number of wp_die() calls in WordPress, a plain text string is passed and the HTML displayed is valid: 

wp_die( 'This is an error message.' );

Currently returns:

<p>This is an error message.</p>.

However, for a number of other wp_die() calls, the HTML displayed is invalid because paragraphs doesn’t allow every nesting possibilities.

For example:

wp_die( '<h1>You need a higher level of permission.</h1><p>Sorry, you are not allowed to manage terms in this taxonomy.</p>' );

Currently returns:

<p><h1>You need a higher level of permission.</h1><p>Sorry, you are not allowed to manage terms in this taxonomy.</p></p>

With WordPress 5.3, error messages are wrapped in a <div> rather than a <p>, to better support string calls in wp_die(), without
outputting invalid HTML.

These changes also add .wp-die-message CSS class for styling.

For example:

wp_die( '<h1>You need a higher level of permission.</h1><p>Sorry, you are not allowed to manage terms in this taxonomy.</p>' );

Will now return:

<div class="wp-die-message">
    <h1>You need a higher level of permission.</h1>
    <p>Sorry, you are not allowed to manage terms in this taxonomy.</p>
</div>

Plugin authors are encouraged to check their use of wp_die() and update their PHP calls to the function or their CSS styles if needed.

For reference, see Trac ticket #47580.

#5-3, #dev-notes

Miscellaneous Developer Focused Changes in 5.3

Oct. 14: Edited to include relevant ticket numbers for each External Library update.
Oct. 15: Corrected the jQueryColor updated from version and added a bullet point about the changes to the random_password filter.

Oct. 29: Added Lodash, React, and ReactDOM to the list of updated external libraries.

In WordPress 5.3, a large handful of small developer-focused changes were made that deserve to be called out. Let’s take a look!

Passing Arrays to Supports Argument When Registering Post Types

When registering post type support for a feature using add_post_type_support(), it’s possible to pass “extra” arguments to provide more details for how that post type should support the feature.

<?php
add_post_type_support(
	'my_post_type',
	array(
		'custom_feature' => array(
			'setting-1' => 'value',
			'setting-2' => 'value',
		),
	)
);

However, passing multidimensional arrays with advanced configuration details to the supports argument in register_post_type() results in the extra arguments getting stripped before being added to the post type.

This has been fixed in WordPress 5.3 and multidimensional arrays can now be passed to supports in register_post_type().

Example

<?php
register_post_type(
	'my_post_type',
	array(
		...
		'supports' => array(
			'title',
			'editor',
			'custom_feature' => array(
				'setting-1' => 'value',
				'setting-2' => 'value',
			),
		),
		...
	)
);

For more information on this change, see #40413 on Trac.

HTML5 Supports Argument for Script and Style Tags

In HTML5, the type attribute is not required for the <script> and <style> tags. Including the attribute on these tags (type="text/javascript", for example) will trigger a validation warning in HTML validation tools.

In WordPress 5.3, two new arguments are now supported for the html5 theme feature, script and style. When these arguments are passed, the type attribute will not be output for those tags.

Example

<?php
function mytheme_register_support() {
	add_theme_support( 'html5', array( 'script', 'style' ) );
}
add_action( 'after_setup_theme', 'mytheme_register_support' );

For more information on this change, see #42804 on Trac.

Recording Additional Information For Saved Queries.

When the SAVEQUERIES constant is set to true, an array of data for every query to the database is logged for debugging purposes. This data includes:

  • The query itself
  • The total time spent on the query
  • A comma separated list of the calling functions
  • The Unix timestamp of the time at the start of the query.

A new filter, log_query_custom_data, has been added to allow custom debugging information to be added to this array of data. A good example where this would be useful is a plugin that runs a custom search implementation (such as Elasticsearch). “This query was served by Elasticsearch” could be added to the debug data to help trace where queries are being served.

Example

<?php
function myplugin_log_query_custom_data( $query_data, $query, $query_time, $query_callstack, $query_start ) {
	$query_data['myplugin'] = 'Custom debug information';
	
	return $query_data;
}
add_filter( 'log_query_custom_data', 'myplugin_log_query_custom_data', 10, 5 );

For more information, see #42151.

Unit-less CSS line-height Values

Line height should also be unit-less, unless necessary to be defined as a specific pixel value.

The WordPress Core CSS Coding Standards

This standard was not followed consistently in the WordPress Core CSS files. All 346 line-height style definitions in Core have been individually examined and adjusted to now adhere to this coding standard.

This change will not cause any issues with Core admin CSS or custom styling, but sites with custom admin styling should be double checked just to be sure.

For more details, see #44643 on Trac, or any of the related sub-tickets (#46488, #46489, #46492, #46493, #46494, #46495, #46509, #46510, #46511, #46512, #46513, #46514, #46515, #46516, #46517, #46518, #46519, #46520, #46521, #46522, #46523, #46524, #46525, #46526, #46528, #46529, #46530, #46531).

Additional Developer Goodies

  • add_settings_error() now supports the warning and info message types in addition to the error and updated/success styles. error and updated types will also now output the .notice-error and .notice-success instead of the legacy .error and .updated classes (see #44941).
  • A typo in the Walker_Nav_Menu_Checklist class has been fixed for the hidden title attribute input field’s class. Previously, the class was .menu-item-attr_title. However, the other fields use a hyphen, which the admin JavaScript also expects. For that reason, the class has been changed to .menu-item-attr-title. This field is used to auto-populate the Title Attribute input field for menu items when adding a menu item. Plugins and themes that extend or that have copied the Walker_Nav_Menu_Checklist class into their code base should also fix this typo (see #47838).
  • An index has been added to wp_options.autoload. While most sites will be completely unaffected by this, sites with a large number of rows in wp_options and a small number of autoload set will see a significant performance improvement (see #24044).
  • The Media Library type filter has been expanded to include document, spreadsheet, and archive filters (see #38195).
  • Autocomplete support has been added for PHP mode in the CodeMirror code editor used when using the plugin and theme editors (see #47769).
  • Support for flex, grid, and column layout techniques have been added to the list of CSS attributes that KSES considers safe for inline CSS (see #37248).
  • The charset used when wp_die() is called is no longer hard coded as utf-8. Instead, utf-8 will be used as a default and a different charset argument can be passed in the third $args parameter for wp_die() (see #46666).
  • The sms protocol has been added to wp_allowed_protocols() (see #39415).
  • Support for a custom WP_PLUGIN_URL has been added to load_script_textdomain(). Plugins may not be on the same host/path as the rest of the content. This ensures translations are loaded correctly in this scenario (see #46336).
  • Three additional parameters ($length, $special_chars, and $extra_special_chars) are now passed to the random_password filter in wp_generate_password(). Previously, code using this filter had no knowledge of the requirements a generated password needed to meet. Now, custom password generators can better utilize the filter instead of overriding the entire pluggable function. (see #47092).

Build/Test Tools

  • A new job on TravisCI test builds has been added to scan for potential PHP compatibility issues (see #46152).
  • Composer scripts will now use the version of PHPCS/PHPCBF installed through Composer, removing the requirement to have them installed globally. The @php prefix has also been added to ensure each script runs on the PHP version that Composer used to install dependencies (see #47853).
  • Installing Grunt globally is no longer required to run build related scripts because Core now uses the local Grunt binary installed by NPM. For example, npm run build can now be used instead. (see #47380).

External Libraries

A number of external libraries and dependencies have been updated. Including Twemoji, which now includes the Transgender symbol and flag!

⚧️ 💖 🏳️‍⚧️

The following external libraries have been updated:

Props @earnjam for peer review.

#5-3, #dev-notes

Improvements in Media component accessibility in WordPress 5.3

Form controls are still unlabelled in WordPress media views. Some don’t have associated <label> element, <aria-label>attribute or some have an empty label.

Properly labeling form controls is essential for a basic level of accessibility, as labels give form controls their accessible name. The name is then used by assistive technologies to inform users what the form control is about. Not to mention visible <label> elements are clickable and help users with motor impairments to set focus on the associated form control.

Also, the WordPress accessibility coding standards recommend explicitly associated labels (with for/id) attributes instead of implicitly (wrapping) labels.

WordPress 5.3 will now include some accessibility improvements of all the media views from controls:

  • Changes the media views form controls to have explicitly associated labels with for/id attributes
  • Adds a few missing labels / aria-labels
  • Improves a few existing labels / aria-labels
  • Improves semantics in a few places, by adding visually hidden headings, fieldset + legend elements, aria-describedby attributes
  • Improves the image custom size input fields and their labeling
  • Adds role=”status” to the “saved” indicator so that status messages are announced to assistive technologies
  • Swaps the columns source order in the image details template, to make visual and DOM order match
  • Swaps the “Replace” and “Back” buttons source order in the Replace Image view, to make visual and DOM order match
  • Gallery settings: move checkbox label to the right: checkboxes are supposed to have labels on the right
  • Merge similar strings, unified to “Drop files to upload” (removed “Drop files here”, and “Drop files anywhere to upload”)
  • Makes the “upload-ui” consistent across the media views
  • Hides the IE 11 “X” ::-ms-clear button in the Insert from URL field, as it conflicts with the uploading spinner
  • Adds comments to all the media templates to clarify their usage
  • Slightly increases the vertical spacing between form fields in the media sidebar
  • Removes some CSS selectors introduced as backwards compatibility for WordPress pre-4.4
  • Removes some CSS still targeting Internet Explorer 7 and 8
  • Fixes buttons group layout for Internet Explorer 11

The most important change to clarify is that the labeling changed from this (implicit labeling):

<label>
    My input
    <input type="text" />
</label>

to this (explicit labeling):

<label for="my-input">My input</label>
<input type="text" id="my-input" />

Simplified code sample


More details on these improvements can be found in Trac ticket #47122.

This note was drafted by @justinahinon and proofread by @afercia.

#5-3, #accessibility, #dev-notes, #media