Overview
Create custom accessibility fixers to extend the 96 built-in fixers with your own accessibility solutions.
Fixer Architecture
All fixers inherit from BaseFixer and implement the fixer interface.
Create a Custom Fixer
Step 1: Create Fixer Class
`php
wcagcriterion(): string
{
return ‘2.5.3’; // Level AAA
}
/**
* Get severity level
*/
public function get_severity(): string
{
return ‘major’; // critical, major, minor
}
/**
* Check if issue exists
*/
public function check_element($element): bool
{
// Check if button needs a label
if ($element->tagName !== ‘button’) {
return false;
}
$text = trim($element->textContent);
$aria_label = $element->getAttribute(‘aria-label’);
// Return true if button has no text and no aria-label
return empty($text) && empty($aria_label);
}
/**
* Fix the accessibility issue
*/
public function fix($content): string
{
// Use DOMDocument to parse
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(
‘‘ . $content,
LIBXMLHTMLNOXMLATT
);
if (!$dom->documentElement) {
return $content;
}
$xpath = new \DOMXPath($dom);
// Find all buttons without text or aria-label
$buttons = $xpath->query(‘//button’);
foreach ($buttons as $button) {
if ($this->check_element($button)) {
// Try to infer label from context
$label = $this->inferbuttonlabel($button);
if (!empty($label)) {
$button->setAttribute(‘aria-label’, $label);
}
}
}
// Return fixed HTML
$fixed = ”;
if ($dom->documentElement) {
foreach ($dom->documentElement->childNodes as $node) {
$fixed .= $dom->saveHTML($node);
}
}
return !empty($fixed) ? $fixed : $content;
}
/**
* Infer button label from context
*/
private function inferbuttonlabel($button): string
{
// Check for icon content
$icon = $button->querySelector(‘i, svg, img’);
if ($icon) {
$classes = $icon->getAttribute(‘class’);
if (strpos($classes, ‘fa-close’) !== false) {
return ‘Close’;
}
if (strpos($classes, ‘fa-menu’) !== false) {
return ‘Menu’;
}
if (strpos($classes, ‘fa-search’) !== false) {
return ‘Search’;
}
}
// Check button position/context
$parent = $button->parentNode;
while ($parent) {
$parent_id = $parent->getAttribute(‘id’);
$parent_class = $parent->getAttribute(‘class’);
if (strpos($parent_class, ‘modal-header’) !== false) {
return ‘Close Dialog’;
}
if (strpos($parent_class, ‘search-form’) !== false) {
return ‘Submit Search’;
}
$parent = $parent->parentNode;
}
return ‘Button’; // Fallback
}
/**
* Get test cases for this fixer
*/
public static function gettestcases(): array
{
return [
[
‘input’ => ‘‘,
‘expected’ => ‘Close’,
‘description’ => ‘Close button with icon’
],
[
‘input’ => ‘‘,
‘expected’ => ‘Button’,
‘description’ => ‘Button with SVG icon’
],
[
‘input’ => ‘‘,
‘expected’ => false, // Don’t fix if already has label
‘description’ => ‘Button already has aria-label’
]
];
}
}
`
Step 2: Register Your Fixer
`php
// In your plugin initialization
addfilter(‘slosaccessibility_fixers’, function($fixers) {
$fixers[‘custom-button-label’] = ‘YourNamespace\Accessibility\Fixers\CustomButtonLabelFixer’;
return $fixers;
});
`
Step 3: Register Test Fixtures
`php
addaction(‘slosfixertests’, function($testrunner) {
$testrunner->registertests(
‘CustomButtonLabelFixer’,
CustomButtonLabelFixer::gettestcases()
);
});
`
BaseFixer Methods
Required Methods
`php
public function get_id(): string
// Return unique fixer identifier
public function get_description(): string
// Return human-readable description
public function fix($content): string
// Return fixed HTML content
public function check_element($element): bool
// Return true if element has issue
`
Optional Methods
`php
public function getwcagcriterion(): string
// Return WCAG reference (e.g., ‘2.5.3’)
// Default: Not specified
public function get_severity(): string
// Return ‘critical’, ‘major’, or ‘minor’
// Default: ‘major’
public function requires_javascript(): bool
// Return true if needs JavaScript fix
// Default: false
public function getperformanceimpact(): string
// Return ‘low’, ‘medium’, ‘high’
// Default: ‘low’
public static function gettestcases(): array
// Return array of test cases
// Default: empty array
`
Fixer Patterns
Pattern 1: Simple Attribute Addition
`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);
$xpath = new \DOMXPath($dom);
$elements = $xpath->query(‘//div[@role=”alert”]’);
foreach ($elements as $element) {
if (!$element->hasAttribute(‘aria-live’)) {
$element->setAttribute(‘aria-live’, ‘polite’);
$element->setAttribute(‘aria-atomic’, ‘true’);
}
}
return $this->getfixedhtml($dom, $content);
}
`
Pattern 2: Element Wrapping
`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);
$xpath = new \DOMXPath($dom);
$buttons = $xpath->query(‘//button[not(@aria-label)]’);
foreach ($buttons as $button) {
$wrapper = $dom->createElement(‘span’);
$wrapper->setAttribute(‘class’, ‘button-accessible’);
$button->parentNode->insertBefore($wrapper, $button);
$wrapper->appendChild($button);
}
return $this->getfixedhtml($dom, $content);
}
`
Pattern 3: Attribute Modification
`php
public function fix($content): string
{
$dom = new \DOMDocument(‘1.0’, ‘UTF-8’);
@$dom->loadHTML(‘‘ . $content);
$xpath = new \DOMXPath($dom);
$images = $xpath->query(‘//img[not(@alt)]’);
foreach ($images as $image) {
$alt = $this->generatealttext($image);
$image->setAttribute(‘alt’, $alt);
}
return $this->getfixedhtml($dom, $content);
}
private function generatealttext($image): string
{
$src = $image->getAttribute(‘src’);
$filename = basename($src, pathinfo($src, PATHINFO_EXTENSION));
return strreplace([‘-‘, ‘‘], ‘ ‘, $filename);
}
`
Pattern 4: CSS Injection
`php
public function requires_css(): bool
{
return true;
}
public function get_css(): string
{
return ‘
/ High contrast mode support /
.focus-indicator:focus {
outline: 3px solid;
outline-offset: 2px;
}
/ Ensure visible focus indicator /
@media (prefers-reduced-motion: no-preference) {
.focus-indicator:focus {
box-shadow: 0 0 0 3px rgba(0,0,0,0.3);
}
}
‘;
}
public function fix($content): string
{
// Add CSS to head if not present
if (strpos($content, ‘focus-indicator-css’) === false) {
$css = ‘
‘;
$content = str_replace(‘‘, $css . ‘‘, $content);
}
return $content;
}
`
Pattern 5: JavaScript Enhancement
`php
public function requires_javascript(): bool
{
return true;
}
public function get_javascript(): string
{
return ‘
document.addEventListener(“DOMContentLoaded”, function() {
// Focus management
document.querySelectorAll(“[data-focus-trap]”).forEach(el => {
const focusableElements = el.querySelectorAll(
“button, [href], input, select, textarea, [tabindex]:not([tabindex=\”-1\”])”
);
if (focusableElements.length > 0) {
el.addEventListener(“keydown”, (e) => {
if (e.key === “Tab”) {
const first = focusableElements[0];
const last = focusableElements[focusableElements.length – 1];
if (e.shiftKey && document.activeElement === first) {
last.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
});
}
});
});
‘;
}
`
Testing Fixers
Create Test Class
`php
class CustomButtonLabelFixerTest extends \WP_UnitTestCase
{
public function testaddsarialabeltoiconbutton()
{
$fixer = new CustomButtonLabelFixer();
$input = ‘‘;
$output = $fixer->fix($input);
$this->assertStringContainsString(‘aria-label=”Close”‘, $output);
}
public function testskipsbuttonwithtext()
{
$fixer = new CustomButtonLabelFixer();
$input = ‘‘;
$output = $fixer->fix($input);
$this->assertStringNotContainsString(‘aria-label’, $output);
}
public function testskipsexistingarialabel()
{
$fixer = new CustomButtonLabelFixer();
$input = ‘‘;
$original = $input;
$output = $fixer->fix($input);
// Should not change existing aria-label
$this->assertStringContainsString(‘aria-label=”Custom”‘, $output);
}
}
`
Run Tests
`bash
cd your-plugin/tests
phpunit –bootstrap=bootstrap.php CustomButtonLabelFixerTest.php
`
Performance Optimization
Best Practices
- Minimize DOM Traversal
- Batch Operations
- Avoid Unnecessary Parsing
- Limit Scope
- Create plugin folder:
my-slos-fixers/ - Create plugin file:
my-slos-fixers.php - Include fixer classes
- Register in initialization
- Package as ZIP
- Distribute via GitHub/WordPress.org
- Identify accessibility issues on your site
- Create custom fixer for that issue
- Test thoroughly
- Register with SLOS
- Monitor effectiveness
- Share with community!
– Use XPath efficiently
– Query only what’s needed
– Cache results if possible
– Group similar fixes
– Modify multiple elements in one pass
– Check for issue before fixing
– Use regex for simple replacements
– Target specific elements
– Skip processed elements
Example Optimization
`php
public function fix($content): string
{
// First check if any issues exist with regex
if (preg_match(‘/
// Only parse if likely to find issues
return $this->parseandfix($content);
}
`
Debugging
Enable Debug Mode
`php
define(‘SLOSFIXERDEBUG’, true);
`
Add Debug Output
`php
public function fix($content): string
{
if (defined(‘SLOSFIXERDEBUG’) && SLOSFIXERDEBUG) {
errorlog(‘Fixing with: ‘ . $this->getid());
error_log(‘Content length: ‘ . strlen($content));
}
// … fix logic
if (defined(‘SLOSFIXERDEBUG’) && SLOSFIXERDEBUG) {
errorlog(‘Fixed content length: ‘ . strlen($fixedcontent));
}
return $fixed_content;
}
`
Distribution
Package as Plugin
Example Plugin File
`php
once plugindir_path(FILE) . ‘fixers/CustomButtonLabelFixer.php’;
addfilter(‘slosaccessibility_fixers’, function($fixers) {
$fixers[‘custom-button-label’] = ‘YourNamespace\Accessibility\Fixers\CustomButtonLabelFixer’;
return $fixers;
});
`
Examples
See the official fixers in:
includes/Modules/AccessibilityScanner/Fixes/Fixers/
Next Steps
Related
Share this article
Still need help?
Our support team is ready to assist you with personalized guidance for your workspace.