--- title: Custom checks description: Add your own accessibility check to Lumtera with a PHP class. It then appears in the editor, reports, settings, WP-CLI and the Abilities API. You can also add block editor quick fixes. --- # Custom checks You can add your own checks to Lumtera. For example, you could enforce a house style, or check the markup of a custom block. A registered check appears everywhere a built-in one does: - the editor sidebar, Elementor panel and review mode - scans on save, the Overview and the Content report - Accessibility → Settings → Checks, where it can be made stricter, softer or switched off - `wp lumtera check`, `scan`, `issues` and `rules`, including the [SARIF and JUnit](/developers/wp-cli#sarif-and-junit) reports for CI - the `lumtera/list-rules` ability - Pro page checks, and the list of checks for Pro [ignore rules](/pro/ignore) ## 1. Write the check A check is a class that extends `Lumtera\AbstractRule` and implements `id()`, `title()`, `category()`, `wcag()`, `how_to_fix()` and `run()`. This example flags expandable `
` sections that have no ``, or an empty one: ```php with a short label as the first child of
, e.g. Shipping details.', 'acme' ); } public function run( \DOMXPath $xpath ): array { $issues = []; foreach ( $this->elements( $xpath, '//details' ) as $details ) { if ( $this->is_hidden( $details ) ) { continue; } $summary = $this->elements( $xpath, './summary', $details )[0] ?? null; if ( null === $summary ) { $issues[] = $this->issue( __( 'This expandable section has no , so its toggle is announced only as "Details".', 'acme' ), $details ); } elseif ( '' === $this->accessible_name( $summary ) ) { // A single finding can carry its own severity. $issues[] = $this->issue( __( 'This expandable section\'s summary is empty.', 'acme' ), $summary, Issue::SEVERITY_ERROR ); } } return $issues; } } ``` ## 2. Register it From a plugin (or mu-plugin): ```php register( new DetailsNoSummary() ); } ); ``` ::: warning Timing Both hooks fire once, on `plugins_loaded` at priority 10. A theme's `functions.php` loads too late for them. From a theme, register the check directly, after Lumtera has loaded: ```php add_action( 'after_setup_theme', function () { if ( class_exists( \Lumtera\Plugin::class ) && \Lumtera\Plugin::instance()->is_booted() ) { require_once __DIR__ . '/inc/class-details-no-summary.php'; \Lumtera\Plugin::instance()->rules->register( new DetailsNoSummary() ); } } ); ``` `is_booted()` is false when Lumtera stopped itself, for example on a server without the PHP DOM extension. ::: ## Replace or remove a built-in check The registry is keyed by ID, so registering a check with a built-in check's ID replaces it. To remove a built-in check completely: ```php add_filter( 'lumtera_rule_classes', fn( $classes ) => array_values( array_diff( $classes, [ \Lumtera\Rules\TextAllCaps::class ] ) ) ); ``` Site owners can also switch any check off under **Settings → Checks**, without code. ## The contract | Method | Returns | | --- | --- | | `id()` | A stable, unique ID. It's stored with findings, dismissals and settings, so never change it. Use lowercase letters, numbers and hyphens, 64 characters at most. Prefix it with your own name. | | `title()` | Short title shown in the issue list | | `category()` | One of the `Wcag::CATEGORY_*` constants (below) | | `wcag()` | The WCAG success criterion, for example `'1.1.1'` | | `level()` | `'A'` (default in `AbstractRule`), `'AA'` or `'AAA'` | | `severity()` | The default severity. In `AbstractRule`, set `protected string $severity`. | | `how_to_fix()` | Plain-language fix, in WordPress terms | | `run( \DOMXPath $xpath )` | An array of `Issue` objects | **Severities:** `Issue::SEVERITY_ERROR` (Error), `Issue::SEVERITY_WARNING` (Needs review), `Issue::SEVERITY_NOTICE` (Tip). Use **Needs review** when a person has to decide. Lumtera is built not to cry wolf. **Categories:** `CATEGORY_IMAGES`, `CATEGORY_LINKS`, `CATEGORY_HEADINGS`, `CATEGORY_FORMS`, `CATEGORY_TABLES`, `CATEGORY_MEDIA`, `CATEGORY_STRUCTURE`, `CATEGORY_COLOR`, `CATEGORY_ARIA`, `CATEGORY_LANGUAGE`. ## What your check sees `run()` gets a `DOMXPath` over the rendered HTML: blocks rendered as on the front end, shortcodes expanded. For post content, that's the content wrapped in ``. For whole pages (Pro page checks, `wp lumtera check` on a full document), it's the whole document. Write queries as `//tag`, not `/html/body/tag`. It doesn't see your theme's CSS. Only inline styles and classes are available. If `run()` throws, the rest of the scan carries on without your check. With `WP_DEBUG` on, the error is written to the PHP error log. Findings the site owner dismissed, or that match a Pro ignore rule, are removed after `run()` returns. Your check doesn't need to handle them. ## Helpers in AbstractRule | Helper | What it does | | --- | --- | | `issue( $message, $node, $severity = null )` | Builds an issue for this check. **Always pass the node**: it gives the snippet and line, maps the issue to its block, and makes the fingerprint that dismissals, Pro tasks and Pro ignore rules use. | | `elements( $xpath, $query, $context = null )` | Runs an XPath query and returns only elements | | `attr( $el, $name )` | Trimmed attribute value, or `''` | | `inside_link( $el )` | Whether the element is inside an `` | | `is_hidden( $el )` | Whether the element or a parent is hidden with `hidden`, `aria-hidden="true"` or an inline `display:none` / `visibility:hidden` | | `is_focusable( $el )` | Whether it can be reached with Tab | | `accessible_name( $el )` | The accessible name, following the W3C order: `aria-labelledby`, `aria-label`, native labelling, text, then `title` | | `subtree_text( $node )` | What a screen reader reads for a subtree (text, alt, aria-label, SVG title) | | `label_text( $control )` | Text of the form control's `