Theme Accessibility and Releasing

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

A theme controls most of the markup and styling a visitor meets, so it decides much of whether a WordPress site can be used with a keyboard, a screen reader or zoomed text. It is also the last step before a theme leaves the developer’s hands: the WordPress.org Theme Directory reviews every theme against a published list of requirements, and private themes need the same discipline even when nobody reviews them. This page covers both halves for the Lumen Press block theme lumen: what the accessibility requirements mean in a block theme and how to test them automatically, then the files, checks and process that take the theme from a folder to a released, updatable package.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 7.4 minimum and 8.3+ recommended, theme.json version 3, WP-CLI 2.12.0, the Theme Check plugin version 20260901, axe-core 4.14.0 and Playwright 1.64.1, and checked on 2026-10-28. The Themes Team rewrote the accessibility-ready requirements on 2026-05-06 and made an accessibility.txt file mandatory for that tag from 2026-06-30; tutorials written before mid-2026 describe the older list. WordPress 7.2 (in beta, due in December 2026) announces no change to theme review so far.

General accessibility principles, WCAG conformance levels and legal requirements are explained in Web Accessibility; this page applies them to WordPress themes. The theme files themselves are introduced in Themes Overview.

What core already does for a block theme

Block themes start with more accessibility built in than classic themes, because core blocks produce the markup:

Feature What core does in 7.1

Skip link

When a block template contains a <main> element, core inserts <a class="skip-link screen-reader-text" id="wp-skip-link">Skip to content</a> before <div class="wp-site-blocks"> and gives <main> the id wp--skip-link--target if it has none. Since 7.0 the link is added on the server by _block_template_add_skip_link(); from 5.8 to 6.9 a script inserted it in the browser. Without a <main>, no skip link is printed.

Landmarks

Group, Template Part and Query blocks take an HTML element (tagName): header, main, section, article, aside or footer. In the Site Editor it is Block settings > Advanced > HTML element.

Navigation

The Navigation block outputs a <nav> with an aria-label (set it per menu), and handles keyboard access to submenus and the overlay menu.

Screen reader text

Core styles the screen-reader-text class (visually hidden, visible on focus) for every block theme; themes reuse the class rather than inventing their own.

Element states

theme.json styles :hover, :focus, :focus-visible and :active for link and button elements, and for more blocks in 7.0/7.1; see theme.json Styles.

The theme still decides the colours, link underlines, focus visibility, heading levels and whether every template has a <main>.

The accessibility requirements, applied to lumen

Every theme in the Theme Directory must meet two accessibility items: a skip link where there is something to skip (menus, sidebars), and visible keyboard focus on links, menus, form fields and buttons, with every control reachable by keyboard at every screen width. Themes that carry the accessibility-ready tag must meet the full list of eighteen items (next section). The Lumen Press theme targets the full list.

A block template gets its skip link and landmarks from the tagName of its blocks. The header and footer template parts become <header> and <footer>, and the content area becomes <main>:

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:post-title {"level":1} /-->
	<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

That is templates/single.html. In parts/header.html the Navigation block is named with {"ariaLabel":"Primary","overlayMenu":"mobile"} so that screen reader users can tell several navigation landmarks apart; the name must not repeat the landmark type ("Primary", not "Primary navigation").

The rendered page, fetched from the test site, starts like this (attributes shortened):

<a class="skip-link screen-reader-text" id="wp-skip-link" href="#wp--skip-link--target">Skip to content</a><div class="wp-site-blocks"><header class="wp-block-template-part">
...
<nav class="is-responsive wp-block-navigation ..." aria-label="Primary" ...>
...
<main id="wp--skip-link--target" class="wp-block-group is-layout-constrained ...">
<h1 class="wp-block-post-title">

banner (<header> at page level), main and contentinfo (<footer> at page level) must each occur once; every template therefore needs exactly one <main>, which is also what core’s skip link looks for. The site title is printed as a paragraph (level: 0) so that each template supplies its own single <h1>: the post title in single.html, the archive or search title in index.html and search.html (Query Title block with level: 1), and a "News" heading in home.html. Heading levels then descend without gaps.

Unhooking wp_enqueue_block_template_skip_link from wp_enqueue_scripts switches core’s link off, for themes that print their own. Classic themes always print their own after wp_body_open(); see Classic Theme Anatomy and The Loop.

The accessibility-ready guidelines ask for a focus indicator at least as visible as a 2 px outline with 3:1 contrast against the background, a focus order that follows the visual order, no keyboard traps, and nothing hidden behind a sticky header when it receives focus. Links inside running text (post content, comments, text in footers) must be underlined, and hovering must change more than the colour. Buttons generated by the Button block need no underline as long as they look different from plain text. In theme.json that is a few element styles:

{
	"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
	"version": 3,
	"styles": {
		"elements": {
			"link": {
				"color": { "text": "var(--wp--preset--color--lumen-red)" },
				"typography": { "textDecoration": "underline" },
				":hover": { "typography": { "textDecoration": "none" } },
				":focus": {
					"outline": { "color": "var(--wp--preset--color--ink)", "offset": "2px", "style": "solid", "width": "2px" }
				}
			},
			"button": {
				"color": { "background": "var(--wp--preset--color--lumen-red)", "text": "var(--wp--preset--color--paper)" },
				":focus": {
					"outline": { "color": "var(--wp--preset--color--ink)", "offset": "2px", "style": "solid", "width": "2px" }
				}
			}
		}
	}
}

The excerpt shows only the styles.elements part of the lumen theme.json; the palette and layout settings are the ones from Themes Overview. Core turns it into :root :where(a:where(:not(.wp-element-button)):focus){outline-color: var(--wp--preset--color--ink);...} and the equivalent rule for .wp-element-button. The outline uses ink rather than lumen-red because a focus outline is measured against the background around the control, and ink on paper is far above 3:1. Never remove focus styles with outline: none (in theme.json or style.css) without replacing them.

Colour contrast

Text must reach WCAG 2.2 AA contrast: 4.5:1 for normal text and 3:1 for large text (about 24 px regular or 19 px bold); icons, form field borders and focus indicators need 3:1. Every palette a theme ships (style variations included) is checked, in every state (hover and focus). Computed with the WCAG relative-luminance formula, the lumen palette gives:

Pair Ratio Allowed use

ink on paper

15.85:1

Any text

lumen-red on paper (or the reverse)

6.91:1

Any text, links, buttons

ink on lumen-red

2.30:1

Nothing: fails even for large text

The table tells the designers what not to allow: ink text on a lumen-red background fails, so the button uses paper on lumen-red (6.91:1), and a style variation such as "Midnight" needs its own run. A lighter red such as #d9534f would reach only 3.74:1 on paper, enough for headings but not for body links. The editor warns when a user picks low-contrast colours for a block, but it cannot stop it; a theme can limit choices by turning off custom colours (settings.color.custom: false), as described in theme.json Settings.

The other accessibility-ready items

The full list, as rewritten in May 2026, has eighteen items. Their detailed test procedures live on the WordPress Accessibility Knowledge Base (wpaccessibility.org), linked from the Themes Team handbook. In short, and what each means for a block theme:

Requirement In a block theme

Skip link to the content

One <main> per template, so core prints the link; it must work on every template, not only the front page.

Landmarks with meaningful names

header, main, footer once each; every <nav> named; all content inside a landmark. A classic theme also declares navigation-widgets in its html5 theme support.

Keyboard navigation

Every control reachable and operable, focus visible (2 px, 3:1), logical order, no traps, at desktop and mobile widths; overlay menus close with Escape.

Controls with accessible names, roles and states

Icon-only buttons (search, menu toggle, social icons) have a text label; toggles expose aria-expanded.

Labelled form fields

Search and comment forms keep their visible labels; placeholders are not labels.

Heading structure

One <h1> per view, no skipped levels in templates and patterns.

Underlined links in text

elements.link underlined; hover changes more than colour.

No ambiguous link text

No bare "Read more" or "Click here"; links to posts carry the post title, visibly or as screen reader text.

Sufficient contrast

4.5:1 text, 3:1 large text and UI, for every palette and state.

Alternative text

Theme images (patterns, logos) have meaningful alt, decorative ones alt="".

Audio, video and animation

Captions where the theme ships media; animations respect prefers-reduced-motion and can be paused.

Reflow, resize, text spacing

Usable at 320 CSS px width and 200% text zoom, and when letter and line spacing are increased; fluid typography helps.

No unexpected change of context

Focusing or changing a field never navigates or submits on its own.

No new windows without warning

Links do not use target="_blank" unless the text says so.

Content on hover or focus

Tooltips and submenus are dismissible, hoverable and persistent.

Accessibility statement

An accessibility.txt file in the theme root (since 2026-06-30).

No inaccessible plugins

The theme does not recommend or require plugins that fail these rules.

Screen reader text

The theme supports the screen-reader-text class, which block themes inherit from core.

The previous list (2014 to 2026) had a separate set of "recommended" items; the 2026 revision dropped them and folded the essentials into the eighteen requirements. The handbook stresses that the tag means the theme meets the Themes Team’s minimum, not that a site built with it conforms to WCAG 2.2 AA: content added by users and plugins can still fail.

The accessibility-ready tag and accessibility.txt

A theme author asks for an accessibility review by adding accessibility-ready to the Tags header before uploading a new theme. Members of the Accessibility team audit it as part of the review, and the theme cannot go live with the tag until it passes; a new theme that fails may still be approved without the tag. For a theme already live, the author requests the review through a form on the handbook page and adds the tag only after approval: adding it unreviewed can lead to suspension. Updates to tagged themes enter an accessibility queue and may be re-reviewed at any time.

Since 2026-06-30 an accessibility-ready theme must ship accessibility.txt in its root, in a fixed format: a statement, quick facts about any audit, how the theme is tested, the screen reader text class, features, and where to ask for help and report problems. The lumen file:

=== Accessibility Statement ===
Lumen is designed and tested to meet the WordPress accessibility-ready
requirements and WCAG 2.2 level AA for its templates and default palette.

== Quick Information ==
Accessibility audit completed: No

== Testing Tools and Methodology ==
Every release is checked with axe-core on the front page, a single book and an
archive, and tested by hand with the keyboard only (Tab, Shift+Tab, Enter,
Escape) at desktop and mobile widths, and with VoiceOver and NVDA.

== Screen Reader Text Class ==
.screen-reader-text

== Accessibility Features ==
* Skip link to the main content on every template.
* Header, main, navigation and footer landmarks; the navigation is named "Primary".
* Links in text are underlined; focus outlines are 2px and contrast 3:1 or more.

== Accessibility Help Contact ==
https://lumenpress.example/accessibility/

== Where to Report Issues ==
https://lumenpress.example/accessibility/report/

The optional audit date, standard, auditor and conformance report lines are added once a formal audit exists.

Testing accessibility automatically

Automated tools find only part of the problems (missing names, contrast, landmarks), so they complement keyboard and screen reader testing, but they are cheap enough to run on every change. The following check opens a set of lumen pages in headless Chrome, runs axe-core’s WCAG 2.2 A/AA and best-practice rules, then presses Tab once and verifies that the first focused element is the skip link pointing at <main>:

// check-a11y.mjs
import { chromium } from 'playwright';
import { AxeBuilder } from '@axe-core/playwright';

const base = process.env.LUMEN_URL ?? 'http://lumenpress.test/';
const paths = ( process.env.LUMEN_PATHS ?? '/,/hello/,/?s=lumen,/no-such-page/' ).split( ',' );

const rules = process.env.PW_HOST_RULES; // e.g. "MAP lumenpress.test 127.0.0.1:8080"
const browser = await chromium.launch( {
	channel: process.env.PW_CHANNEL, // "chrome" uses an installed Google Chrome
	args: rules ? [ `--host-resolver-rules=${ rules }` ] : [],
} );
const context = await browser.newContext();
const page = await context.newPage();
let failures = 0;

for ( const path of paths ) {
	const url = new URL( path, base ).href;
	await page.goto( url );

	// 1. Automated WCAG 2.2 A/AA and best-practice rules, before anything has focus.
	const results = await new AxeBuilder( { page } )
		.withTags( [ 'wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice' ] )
		.analyze();
	for ( const v of results.violations ) {
		console.log( `  ${ v.impact } ${ v.id }: ${ v.help } (${ v.nodes.length })` );
	}
	failures += results.violations.length;

	// 2. The skip link is the first focusable element and targets <main>.
	await page.keyboard.press( 'Tab' );
	const first = await page.evaluate( () => {
		const el = document.activeElement;
		return { text: el.textContent.trim(), href: el.getAttribute( 'href' ) };
	} );
	const targets = first.href?.startsWith( '#' )
		? await page.locator( `main${ first.href }` ).count()
		: 0;
	if ( first.text !== 'Skip to content' || targets !== 1 ) {
		failures++;
	}
	console.log(
		`${ url } axe ${ results.testEngine.version }: ${ results.violations.length } violations, ` +
		`${ results.passes.length } rules passed; first Tab: "${ first.text }" -> ${ first.href }`
	);
}

await browser.close();
process.exit( failures ? 1 : 0 );

The script runs as an ES module (check-a11y.mjs). The test site is the standard local stack from Local Development Environments, with lumen mounted into wp-content/themes/, a published post "Hello" containing a link in its text, and pretty permalinks. PW_HOST_RULES lets the browser reach lumenpress.test on the port the stack publishes (8080 here) without editing the hosts file; PW_CHANNEL=chrome uses an installed Chrome instead of downloading Playwright’s browser (npx playwright install chromium is the alternative):

npm install --save-dev --save-exact @axe-core/playwright@4.14.0 playwright@1.64.1
PW_CHANNEL=chrome PW_HOST_RULES="MAP lumenpress.test 127.0.0.1:8080" node check-a11y.mjs
http://lumenpress.test/ axe 4.14.0: 0 violations, 32 rules passed; first Tab: "Skip to content" -> #wp--skip-link--target
http://lumenpress.test/hello/ axe 4.14.0: 0 violations, 33 rules passed; first Tab: "Skip to content" -> #wp--skip-link--target
http://lumenpress.test/?s=lumen axe 4.14.0: 0 violations, 32 rules passed; first Tab: "Skip to content" -> #wp--skip-link--target
http://lumenpress.test/no-such-page/ axe 4.14.0: 0 violations, 42 rules passed; first Tab: "Skip to content" -> #wp--skip-link--target

To see the check fail, the same templates were copied with the content group’s tagName changed from main to the default div, links set to textDecoration: none and the post titles left as the only headings. The result:

  moderate landmark-one-main: Document should have one main landmark (1)
  moderate page-has-heading-one: Page should contain a level-one heading (1)
  moderate region: All page content should be contained by landmarks (1)
http://lumenpress.test/ axe 4.14.0: 3 violations, 27 rules passed; first Tab: "Lumen Press" -> http://lumenpress.test

Three lessons come out of these runs. The missing skip link was caught only by the Tab check, not by axe, since bypass passes whenever any landmark or heading exists. The landmark and heading problems are best-practice rules, so a run limited to the wcag2* tags reports zero violations for a theme without <main>. And the order matters: a first version that ran axe after pressing Tab reported a target-size failure, because the focused skip link sits on top of the site title link at the top left. Run axe on the untouched page, then test keyboard behaviour. The same checks fit into an end-to-end suite, as described in Accessibility Checks in E2E Tests.

Manual checks no tool replaces: tab through every template at desktop and mobile widths (overlay menu included), zoom to 200% and 400%, read pages with VoiceOver or NVDA, and repeat for each style variation, using the Theme Unit Test data (WordPress/theme-test-data) that reviewers also use.

Theme Directory review requirements

Every theme uploaded to wordpress.org/themes is reviewed by volunteers of the Themes Team against the "Required" list in their handbook. A theme with three or more distinct problems may be closed and must be resubmitted after fixing them. The requirements, grouped and summarised:

Area Requirement

Accessibility

Skip link (block themes get it from core) and visible keyboard focus everywhere; the full list only with the accessibility-ready tag.

Code

No PHP or JavaScript errors, warnings or notices; no deprecated functions; data validated and sanitised on input and escaped on output; a unique prefix of at least four letters for everything in the global namespace (functions, options, constants, handles, image sizes, pattern categories).

Core behaviour

Notices through admin_notices and dismissible; no paywalled WordPress features; no hiding the admin bar; no redirect on activation; no removal of non-presentational hooks.

Presentation only

No custom post types, custom blocks, roles, shortcodes, analytics, SEO settings, contact forms or social sharing: those are plugin territory.

Documentation

A valid readme.txt (next section) and a complete style.css header.

Internationalisation

All user-facing strings translatable; text domain equal to the slug; at most two text domains when a framework is bundled.

Licensing

Everything in the zip GPL-compatible (GPLv2 or later recommended), with copyright and licence of the theme and of every bundled resource listed in one file; no cloned designs; the front-end copyright notice belongs to the site owner.

Naming

No "WordPress", "Theme" or "Twenty" in the name; "WordPress" spelled with a capital P; no trademarks and no WordPress logo.

Options

Settings and Options APIs; a single option prefixed with the slug; admin pages under Appearance, capability-checked, and loading assets only on their own screen.

Plugins

Only WordPress.org plugins may be recommended, installed by the user; no bundled plugin zips.

Privacy

No tracking unless opt-in; data collection documented in readme.txt.

Credits and selling

One front-end credit link (to the Theme URI or Author URI) plus one to WordPress.org; no obtrusive upselling; affiliates disclosed.

Assets

Unminified sources next to minified files; core’s bundled libraries (jQuery and others) used instead of copies; no CDN or remote resources without consent (Google Fonts is the only listed exception, though self-hosting is better for privacy).

Templates

Block themes: style.css, readme.txt, theme.json and templates/index.html, with complete markup. Classic themes: the required template functions and theme supports.

Files

No .git/.svn folders, hidden files, __MACOSX, .sql, .sh, zips, php.ini or most .xml files; one line-ending style.

Screenshot

Not an advertisement; at most 1200 × 900 px, 4:3.

Uploading

One new theme in the queue per author at a time; no reserving names.

The process: upload, new-theme queue, a reviewer, findings on a Trac ticket, fixes uploaded as new versions, approval, a final check by a key reviewer, then live. A ticket idle on the author’s side for seven days may be closed; there are no promised timeframes.

readme.txt

readme.txt uses the same format as plugin readmes but different rules. The lumen readme keeps the header, short description and == Description == shown in Themes Overview; version 0.2.0 continues it with (plus a one-question FAQ stating that the theme collects no data):

== Changelog ==

= 0.2.0 =
* Underlined links, visible focus outlines, header and footer landmarks.
* Added accessibility.txt and screenshot.png.

= 0.1.0 =
* First skeleton: style.css, theme.json and the index template.

== Upgrade Notice ==

= 0.2.0 =
Improves keyboard focus and link visibility.

== Copyright ==

Lumen WordPress Theme, (C) 2026 Lumen Press.
Lumen is distributed under the terms of the GNU GPL v2 or later.

== Resources ==

* screenshot.png, (C) 2026 Lumen Press, GPLv2 or later.

The rules the review applies: Contributors holds one WordPress.org username; Requires at least, Tested up to, Requires PHP, License and License URI are present; the short description after the header is at most 150 characters; each upgrade notice is at most 300 characters; the sections cover the description, FAQ, changelog, upgrade notice and resources; and any data collection is documented. Unlike plugin readmes, theme readmes have no Stable tag: the version comes from style.css. Theme Check does not verify all of these, so Lumen Press keeps a small check in its release script:

<?php
// Checks a theme's readme.txt against the Theme Directory's documented rules.
// Usage: php check-theme-readme.php path/to/theme
$dir    = rtrim( $argv[1] ?? '.', '/' );
$readme = str_replace( "\r\n", "\n", (string) @file_get_contents( "$dir/readme.txt" ) );
$errors = array();

// "=== Name ===", then "Field: value" lines, a blank line and the short description.
if ( ! preg_match( '/^=== .+ ===\n((?:[^\n]+\n)+)\n([^\n=][^\n]*)/', $readme, $m ) ) {
	fwrite( STDERR, "No valid readme.txt header in $dir\n" );
	exit( 2 );
}
$fields = array();
foreach ( explode( "\n", trim( $m[1] ) ) as $line ) {
	$parts = array_map( 'trim', explode( ':', $line, 2 ) );
	$fields[ $parts[0] ] = $parts[1] ?? '';
}
foreach ( array( 'Contributors', 'Requires at least', 'Tested up to', 'Requires PHP', 'License', 'License URI' ) as $key ) {
	if ( empty( $fields[ $key ] ) ) {
		$errors[] = "Header field \"$key\" is missing.";
	}
}
if ( str_contains( $fields['Contributors'] ?? '', ',' ) ) {
	$errors[] = 'Contributors must be a single WordPress.org username.';
}
if ( strlen( $m[2] ) > 150 ) {
	$errors[] = sprintf( 'The short description has %d characters; the limit is 150.', strlen( $m[2] ) );
}
foreach ( array( 'Description', 'Changelog', 'Resources' ) as $section ) {
	if ( ! str_contains( $readme, "== $section ==" ) ) {
		$errors[] = "Section \"== $section ==\" is missing.";
	}
}
if ( preg_match( '/== Upgrade Notice ==\n(.*?)(?=\n== |\z)/s', $readme, $u ) ) {
	foreach ( preg_split( '/^= .+ =$/m', $u[1], -1, PREG_SPLIT_NO_EMPTY ) as $notice ) {
		if ( strlen( trim( $notice ) ) > 300 ) {
			$errors[] = 'An upgrade notice is longer than 300 characters.';
		}
	}
}
// The Version in style.css needs its own changelog entry.
$css = (string) @file_get_contents( "$dir/style.css" );
if ( preg_match( '/^\s*Version:\s*(\S+)/mi', $css, $v ) && ! str_contains( $readme, "= {$v[1]} =" ) ) {
	$errors[] = "style.css says Version {$v[1]}, but the changelog has no \"= {$v[1]} =\" entry.";
}

echo $errors ? 'FAIL ' . implode( "\nFAIL ", $errors ) . "\n" : "readme.txt OK\n";
exit( $errors ? 1 : 0 );

Run on lumen and on a copy with two contributors, no Requires PHP, a 169-character description and a version bump missing from the changelog:

$ php check-theme-readme.php wp-content/themes/lumen
readme.txt OK
$ php check-theme-readme.php broken
FAIL Header field "Requires PHP" is missing.
FAIL Contributors must be a single WordPress.org username.
FAIL The short description has 169 characters; the limit is 150.
FAIL style.css says Version 0.2.0, but the changelog has no "= 0.2.0 =" entry.

Screenshot, licensing and credits

screenshot.png sits in the theme root. The review accepts any 4:3 image up to 1200 × 900 px; 1200 × 900 is preferred for high-density displays, and older tutorials quoting 880 × 660 or 600 × 450 describe earlier limits. The image shows the theme as it looks with its default settings, not a marketing banner, and must itself be GPL-compatible: a screenshot containing stock photos needs those photos' licences listed under Resources. PNG is the usual choice; core also finds .jpg, .jpeg, .gif, .webp and .avif.

Licensing applies to the whole package: PHP, JavaScript, CSS, fonts, icons and images. A bundled font under the SIL Open Font License or an icon set under MIT is fine, because both are GPL-compatible, but each needs its name, copyright holder, licence and source URL in readme.txt (or a single resources file). Public-domain (CC0) images need only the source. Premium versions of a theme sold elsewhere must also be GPL-compatible, and the selling site must say so. Credits are limited to one footer link to the theme or author page, plus one to WordPress.org.

Theme Check

The Theme Check plugin runs the automated checks the directory runs on every upload; a theme that fails them is rejected before a human sees it. It adds Appearance > Theme Check to the admin and (version 20260901 included) a WP-CLI command. On the Lumen Press test site:

wp plugin install theme-check --activate
wp theme-check run lumen            # --format=json for scripts

lumen passes: the output is two INFO lines (the accessibility-ready review notice, and a text-domain note with an empty domain, expected while the theme has no PHP strings) and the exit code is 0. A copy named "Lumen WordPress Theme", with no Requires PHP, a 1600 × 900 screenshot and a functions.php that registers lp_book and a shortcode produces REQUIRED results and exit code 1, which is what a release script tests for:

REQUIRED	The theme uses the register_post_type() function in the file functions.php. register_post_type() is plugin-territory functionality and must not be used in themes. Use a plugin instead. Line 5: ...
REQUIRED	The theme uses the add_shortcode() function in the file functions.php. add_shortcode() is plugin-territory functionality and must not be used in themes. Use a plugin instead. Line 6: ...
REQUIRED	Screenshot is wrong size! Detected: 1600x900. Maximum allowed size is 1200x900px.
REQUIRED	Screenshot dimensions are wrong! Ratio of width to height should be 4:3.
RECOMMENDED	Screenshot size should be 1200x900, to account for HiDPI displays. Any 4:3 image size is acceptable, but 1200x900 is preferred.
REQUIRED	Requires PHP: is missing from your style.css header.
WARNING	Your theme appears to be in the wrong directory for the theme name. The directory name must match the slug of the theme. This theme's correct slug and text-domain is lumen-wordpress-theme. (If this is a child theme, you can ignore this error.)

The same broken copy also loaded a script from a CDN, lacked Tested up to in readme.txt, had "WordPress" in its name and contained a .DS_Store file, and the WP-CLI run reported none of these. Passing Theme Check is necessary, not sufficient: the human review checks the rest, and the readme check above and a clean packaging step cover some of it. Accessibility is not checked by Theme Check at all beyond the informational line.

Packaging and submitting

The release zip contains one folder named after the slug, with sources but without development files. When the theme lives in Git, git archive with export-ignore rules in .gitattributes is the cleanest way; without Git, zip with exclusions:

cd wp-content/themes
zip -r lumen-0.2.0.zip lumen -x '*/.*' '*/node_modules/*' 'lumen/package*.json' '*/__MACOSX/*'
unzip -l lumen-0.2.0.zip

The listing then holds only the theme files, without hidden files, node_modules or package.json. A theme with a build step (Sass, JavaScript bundled with @wordpress/scripts) ships both the built files and their sources, as the review requires, but not node_modules. Before uploading: test with the Theme Unit Test data and WP_DEBUG on, run Theme Check, run the accessibility checks, test against the next WordPress beta (the WordPress Beta Tester plugin or wp core update --version=nightly on a test site), and check that Version in style.css matches the newest changelog entry. The upload form is at wordpress.org/themes/upload and needs a WordPress.org account; the zip goes into the new-theme queue described above.

Lumen Press keeps lumen private; the same checks apply, and the Update URI header (see Themes Overview) stops WordPress.org offering a same-slug theme as an "update".

Versioning and updates

WordPress compares the Version header of an installed theme with the version the directory reports and offers an update when the directory’s is higher (version_compare() semantics). Versions are X.Y or X.Y.Z, each higher than the last: patch for fixes, minor for new patterns or templates, major for changes to existing layouts.

A live directory theme is updated in one of two ways:

  • uploading a new zip with a higher Version through the same upload form (also the way to fix issues while the theme is still in review, without losing its queue position), or

  • committing to the theme’s Subversion repository at themes.svn.wordpress.org/<slug>/, where every release is a new folder named after the version (0.2.0/), usually created by copying the previous one with svn cp.

svn checkout https://themes.svn.wordpress.org/<theme-slug>/ theme-svn
cd theme-svn
svn cp 0.1.0 0.2.0
# copy the new release files over 0.2.0/, then:
svn status
svn commit -m "Release 0.2.0"

Subversion is a release channel, not a development repository: a committed version cannot be changed afterwards, and a mistake is fixed only by releasing a higher version. Development stays in Git; the export steps are described in Create Block Theme and Version Control. Updates to an accessibility-ready theme go through the accessibility queue as well.

On the site side, theme updates replace the whole folder, so local edits to theme files are lost: customisations belong in the Site Editor (stored in the database), a child theme (Child Themes) or a plugin. Templates and styles a user changed in the Site Editor keep winning over the updated files until reset. Since 5.5 administrators can enable automatic updates per theme on Appearance > Themes; the policies and filters are on Updates and Auto-Updates.

What older books get wrong or leave out

Older advice Current state (7.1)

Accessibility is a matter of choosing an "accessibility-ready" theme from the directory, or adding an accessibility plugin or overlay

The tag means the theme met the Themes Team’s minimum; content, plugins and later edits still need testing. Overlays do not fix theme markup. Contrast, headings and link styling are design decisions taken in theme.json.

A theme adds its own skip link script, or block themes have none

Core adds the skip link to block templates with a <main> element (by script from 5.8, on the server since 7.0). The theme’s job is to have exactly one <main> in every template.

The accessibility-ready list has required and recommended items

Since May 2026 there are eighteen required items and no recommendations; tagged themes also need accessibility.txt (from June 2026).

Screenshot size 880 × 660 or 600 × 450

Up to 1200 × 900, 4:3, with 1200 × 900 preferred.

A theme can bundle plugins, register post types for its demos, or load fonts and libraries from CDNs

All three fail the review; post types and shortcodes are plugin territory and remote resources need consent.

Buying a premium theme means the code is proprietary

Themes are derivative of WordPress and distributed under GPL-compatible licences; premium sellers license support and updates, not exclusive rights to the code.

Common errors and pitfalls

Symptom Cause and fix

No "Skip to content" link on some pages

Those templates (often a custom page template or 404.html) wrap their content in a div group. Set the group’s HTML element to main.

Two skip links

The theme prints its own and core’s is still hooked. Remove the theme’s link, or unhook wp_enqueue_block_template_skip_link deliberately.

axe reports target-size on the site title

The check ran after Tab was pressed and the focused skip link overlaps the header. Run axe on the unfocused page.

axe passes, but reviewers report landmark and heading problems

The run used only wcag2* tags. Add best-practice (region, landmark-one-main, page-has-heading-one) and test keyboard behaviour separately.

Focus outline invisible on buttons

Only elements.link has a :focus style, or style.css sets outline: none. Add elements.button :focus styles with 3:1 contrast.

Review closed after three issues

Fix every reported item, bump Version, add a changelog entry and resubmit.

Update not shown to sites

Version was not increased, or the SVN commit went into an existing version folder. Release a new, higher version.

Upload rejected for hidden files

.DS_Store, .git or __MACOSX were zipped. Package with exclusions or git archive.

Security notes: the review’s code rules double as a security checklist for any theme (escape output, sanitise input, check capabilities on admin pages, no remote code or eval). A theme sold or shared outside WordPress.org gets no review, so the same Theme Check and manual checks are worth running on it before installing, as described in Choosing and Managing Plugins and Themes and Security Hardening.