This guide explains how to integrate external plugins and custom blocks with the Block Accessibility Checks Validation API, enabling dedicated accessibility validation for your blocks.
Upgrading from v3? The registration API changed significantly in v4.0.0. Jump to Upgrading from v3 for a complete before/after reference.
External plugins register validation checks in PHP (configuration) and implement the validation logic in JavaScript (real-time feedback). Registered checks automatically appear in the unified Block Accessibility Checks settings table, attributed to your plugin via the required namespace field.
Use ba11yc_register_block_check() inside the ba11yc_ready action. The namespace and name keys are required:
<?php
add_action( 'ba11yc_ready', function() {
ba11yc_register_block_check(
'your-plugin/your-block',
array(
'namespace' => 'your-plugin',
'name' => 'check_name',
'error_msg' => __( 'Error message', 'your-text-domain' ),
'warning_msg' => __( 'Warning message', 'your-text-domain' ),
'description' => __( 'Check description', 'your-text-domain' ),
'level' => 'error', // default severity: 'error', 'warning', or 'none'
'configurable' => true, // true = admin can change the level in settings
'category' => 'accessibility', // 'accessibility' or 'validation'
)
);
} );All validation logic runs in JavaScript for real-time feedback. Use the ba11yc.validateBlock filter:
import { addFilter } from '@wordpress/hooks';
addFilter(
'ba11yc.validateBlock',
'my-plugin/validation',
(isValid, blockType, attributes, checkName) => {
if (blockType !== 'my-plugin/card-block') {
return isValid;
}
switch (checkName) {
case 'check_title':
return !!(attributes.title && attributes.title.trim());
case 'check_image_alt':
return !!(attributes.imageAlt && attributes.imageAlt.trim());
default:
return isValid;
}
}
);namespace(string) — Your plugin's identifier. Used to attribute checks to your plugin in the DataViews settings table.name(string) — Unique check name within the block type.error_msg(string) — Message shown when the check fails.
warning_msg(string) — Warning message (defaults toerror_msg).description(string) — Description shown in the settings UI.level(string) — Default severity:'error','warning', or'none'. Default:'error'.configurable(bool) — Whether the admin can change the level in settings. Default:true.category(string) —'accessibility'or'validation'. Used as a filter in the settings table.priority(int) — Execution order (lower = earlier). Default:10.enabled(bool) — Whether the check is active. Default:true.
| Goal | level |
configurable |
|---|---|---|
| User-configurable check (starts as error) | 'error' |
true |
| User-configurable check (starts as warning) | 'warning' |
true |
| Fixed error, not configurable | 'error' |
false |
| Fixed warning, not configurable | 'warning' |
false |
| Disabled by default | 'none' |
true |
Checks with configurable: false are omitted from the settings table (they cannot be changed).
The heading order check (check_heading_rank) compares every heading in the document, not just
core/heading blocks. For it to see a heading your block renders, it has to know the level.
Two kinds of block are recognized automatically:
- Core's heading blocks —
core/post-title,core/site-title,core/query-title,core/comments-title,core/site-tagline,core/term-name,core/widget-group, andcore/accordion-heading. - Any block storing its heading level in a numeric
levelattribute. This is the convention core itself uses, where0means the block renders a paragraph rather than a heading. If your block already offers a heading level control that way, it works with no registration.
Check what was detected from the browser console in the editor:
wp.data.select( 'core/editor' ).getEditorSettings().blockA11yChecks.headingSources;For a block that renders a heading some other way, register it on ba11yc_ready:
add_action( 'ba11yc_ready', function () {
// A fixed level.
ba11yc_register_heading_source( 'my-plugin/section', array( 'level' => 2 ) );
// The level is chosen by the user, stored under a different attribute name.
ba11yc_register_heading_source( 'my-plugin/hero', array(
'attribute' => 'headingLevel',
'level' => 2, // used when the attribute is unset
) );
// The attribute holds a token rather than a number.
ba11yc_register_heading_source( 'my-plugin/callout', array(
'attribute' => 'size',
'map' => array( 'large' => 2, 'medium' => 3, 'small' => 4 ),
'level' => 3,
) );
// The heading only exists when an optional field has content. Without this,
// an empty block would still be counted as rendering a heading.
ba11yc_register_heading_source( 'my-plugin/panel', array(
'level' => 2,
'requires' => 'title',
) );
// The block renders no heading. Also useful to exclude a block that would
// otherwise be detected.
ba11yc_register_heading_source( 'my-plugin/quiet', array( 'level' => 0 ) );
// Several blocks sharing one shape — the common case in a block library.
ba11yc_register_heading_source(
array( 'my-plugin/hero', 'my-plugin/card-list' ),
array(
'attribute' => 'headingLevel',
'level' => 2,
)
);
} );This works for any block, including ones belonging to another plugin, which is why it is the recommended surface.
If you own the block and would rather keep the declaration with it:
{
"supports": {
"ba11yc": {
"headingLevel": { "level": 2, "requires": "title" }
}
}
}A bare number works as shorthand ("headingLevel": 2), and false means the block renders no
heading. PHP registration overrides anything declared here.
When the level cannot be expressed as data — it depends on nesting depth, on sibling state, or on several attributes at once — use the JavaScript filter, which overrides everything above:
addFilter(
'ba11yc.blockHeadingLevels',
'my-plugin/heading-levels',
( levels, block ) => {
if ( block.name !== 'my-plugin/section' ) {
return levels;
}
return [ block.attributes.depth + 1 ];
}
);Return [] for no heading, or several entries for a block that renders more than one.
- A declared block is also flagged when it is the block that skips a level, not just counted as context for the headings around it.
- No settings row is created. Severity follows the single Heading level order row, so turning that off turns it off everywhere.
Checks with 'configurable' => true appear in the unified DataViews settings table under Block Accessibility Checks. Admins can filter by your namespace to see only your plugin's checks. There are no per-plugin submenus — all plugins share the single table.
<?php
/**
* Plugin Name: My Custom Blocks
* Version: 1.0.0
* Text Domain: my-custom-blocks
*/
class MyCustomBlocksIntegration {
public function __construct() {
add_action( 'ba11yc_ready', array( $this, 'register_checks' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_validation_script' ) );
}
public function register_checks() {
// Card block — title check
ba11yc_register_block_check(
'my-custom-blocks/card',
array(
'namespace' => 'my-custom-blocks',
'name' => 'check_title',
'error_msg' => __( 'Card title is required.', 'my-custom-blocks' ),
'warning_msg' => __( 'Consider adding a title for better accessibility.', 'my-custom-blocks' ),
'description' => __( 'Card title validation', 'my-custom-blocks' ),
'level' => 'error',
'configurable' => true,
'category' => 'accessibility',
)
);
// Card block — image alt text check
ba11yc_register_block_check(
'my-custom-blocks/card',
array(
'namespace' => 'my-custom-blocks',
'name' => 'check_image_alt',
'error_msg' => __( 'Card image requires alt text.', 'my-custom-blocks' ),
'warning_msg' => __( 'Alt text is recommended for card images.', 'my-custom-blocks' ),
'description' => __( 'Image alt text validation', 'my-custom-blocks' ),
'level' => 'warning',
'configurable' => true,
'category' => 'accessibility',
)
);
}
public function enqueue_validation_script() {
$asset_file = include plugin_dir_path( __FILE__ ) . 'build/validation.asset.php';
// Start with base dependencies.
$dependencies = $asset_file['dependencies'];
// Only add Block Accessibility Checks as a dependency if it's active.
// This allows your plugin to work even when BAC is deactivated.
if ( wp_script_is( 'block-accessibility-script', 'registered' ) ) {
$dependencies[] = 'block-accessibility-script';
}
wp_enqueue_script(
'my-custom-blocks-validation',
plugins_url( 'build/validation.js', __FILE__ ),
$dependencies,
isset( $asset_file['version'] ) ? $asset_file['version'] : '1.0.0',
true
);
}
}
new MyCustomBlocksIntegration();// build/validation.js (src)
import { addFilter } from '@wordpress/hooks';
addFilter(
'ba11yc.validateBlock',
'my-custom-blocks/validation',
(isValid, blockType, attributes, checkName) => {
if (blockType !== 'my-custom-blocks/card') {
return isValid;
}
switch (checkName) {
case 'check_title':
return !!(attributes.title && attributes.title.trim());
case 'check_image_alt':
return !!(attributes.imageAlt && attributes.imageAlt.trim());
default:
return isValid;
}
}
);Registered checks automatically appear in the block editor:
- Unified Sidebar — All validation issues consolidated in one panel (click the accessibility icon in the toolbar).
- Block Indicators — Blocks with issues display a badge. Clicking it shows a popover with details.
- Header Badge — Total error/warning count shown in the editor toolbar.
- Post Locking — Checks resolving to
'error'level prevent publishing until resolved.
Use a consistent namespace for all blocks from your plugin:
// Good: consistent namespace
'your-plugin/block-one'
'your-plugin/block-two'
// Avoid: inconsistent namespaces — they appear as separate plugins in the settings table
'plugin1/block-one'
'plugin2/block-two'- Use unique block type and check names to avoid conflicts with other plugins.
- Register checks in the
ba11yc_readyaction. - Provide clear, actionable error and warning messages.
- Keep your JavaScript validation logic simple and fast.
- Use the same
namespacevalue for every check in your plugin so they group together in the settings table.
v4.0.0 is a clean-break release. There are no compatibility shims — all integrations must update. This section covers block checks; for the complete migration reference (meta and editor checks, the data store, REST changes, and the settings migration), see the Upgrading to v4.0 guide.
The $registry->register_block_check() method called on the $registry passed to ba11yc_ready is replaced by the global ba11yc_register_block_check() function. The check name moves from its own parameter into $args['name'].
Before (v3):
add_action( 'ba11yc_ready', function( $registry ) {
$registry->register_block_check(
'my-plugin/card',
'check_title', // second positional param
array(
'error_msg' => 'Title required.',
'type' => 'settings',
'category' => 'accessibility',
)
// optional 4th $plugin_info array was here
);
} );After (v4):
add_action( 'ba11yc_ready', function() { // $registry param no longer needed
ba11yc_register_block_check(
'my-plugin/card',
array(
'namespace' => 'my-plugin', // required
'name' => 'check_title', // moved into $args
'error_msg' => 'Title required.',
'level' => 'error', // replaces 'type'
'configurable' => true, // replaces type:'settings'
'category' => 'accessibility',
)
);
} );The 'type' key is removed. Replace it with 'level' (default severity) and 'configurable' (whether the admin can change it).
v3 type |
v4 level |
v4 configurable |
|---|---|---|
'settings' |
'error' |
true |
'error' |
'error' |
false |
'warning' |
'warning' |
false |
'none' |
'none' |
false |
Before (v3):
addFilter(
'ba11yc_validate_block', // underscore-separated
'my-plugin/validation',
(isValid, blockType, attributes, checkName) => { /* ... */ }
);After (v4):
addFilter(
'ba11yc.validateBlock', // dot-separated camelCase
'my-plugin/validation',
(isValid, blockType, attributes, checkName) => { /* ... */ }
);Before (v3):
const rules = window.BlockAccessibilityChecks?.validationRules || {};After (v4):
import { select } from '@wordpress/data';
const { blockA11yChecks } = select( 'core/editor' ).getEditorSettings();Previously, each plugin with type: 'settings' checks got an auto-generated submenu page under the Block Accessibility Checks admin menu. Those submenus are gone. Your checks now appear in the unified DataViews settings table alongside all other plugins' checks. Filter by your namespace to see only your checks.
No code change is required for this — it's purely a UI change. If you had bookmarked URLs pointing to old submenu slugs (block-a11y-checks-{your-slug}), they redirect to the unified page.
- Confirm
ba11yc_register_block_check()is called insideba11yc_ready. - Ensure
'namespace','name', and'error_msg'are all present in$args. - Checks with
'configurable' => falseare intentionally excluded from the table.
All checks from your plugin should share the same namespace value. If they appear under different groups, different namespace values are being passed.
Ensure your JavaScript file is enqueued on enqueue_block_editor_assets and includes block-accessibility-script as a dependency (when registered).