A client forwarded me a demand letter two years ago. It was a three-page document from a law firm representing a visually impaired plaintiff, alleging that their self-storage tenant portal violated the Americans with Disabilities Act because a screen reader user couldn't complete an online payment. The letter cited specific WCAG 2.1 criteria. It demanded remediation within thirty days and threatened federal litigation. My client had no idea what WCAG was. By the end of that week, we both knew it well.
ADA accessibility litigation against websites has been growing steadily for years. Serial plaintiffs and their law firms have automated discovery — they run accessibility scanners against thousands of sites and send demand letters to the ones that fail. PHP-built applications, particularly custom billing and tenant portal software, are common targets because they tend to be built by developers focused on functionality rather than accessibility. This guide covers what WCAG 2.1 AA actually requires, where PHP-built apps typically fail, and how to fix the most common violations.
What the Law Actually Says
The ADA doesn't mention websites explicitly — it predates the commercial web. Courts have increasingly interpreted Title III, which covers public accommodations, to include websites and web applications. The Department of Justice issued guidance in 2022 confirming that web accessibility is required under the ADA and that WCAG 2.1 Level AA is the appropriate technical standard.
A demand letter typically cites specific WCAG success criteria by number, lists specific pages or elements that failed, and gives you a remediation window before they file in federal court. The settlements I've seen for small business websites typically run $5,000–$25,000 plus legal fees. Remediation is cheaper than litigation. Start with the most common violations.
The Most Common Violations in PHP Applications
Missing Alt Text on Images (WCAG 1.1.1)
Every meaningful image needs descriptive alt text. Decorative images need an empty alt attribute (alt="") so screen readers skip them. In PHP templates, the failure mode is usually a dynamic image output where the developer didn't include an alt attribute:
<!-- Violation -->
<img src="<?= htmlspecialchars($unit['photo_url']) ?>">
<!-- Correct -->
<img src="<?= htmlspecialchars($unit['photo_url']) ?>"
alt="<?= htmlspecialchars($unit['unit_number'] . ' — ' . $unit['unit_type']) ?>">
<!-- Decorative / icon image -->
<img src="/assets/icons/check.svg" alt="" role="presentation">
In billing applications, the common offenders are receipt icons, status indicator images, and facility photos used as backgrounds that are rendered via <img> tags instead of CSS.
Poor Color Contrast (WCAG 1.4.3)
WCAG 2.1 AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. Billing applications consistently fail on secondary text — due date labels in gray, status badges with light-colored text, disabled form field text. Use the WebAIM Contrast Checker to verify every text/background combination. Common failure pattern in billing UIs:
<!-- Violation — #999 on #fff is 2.85:1 -->
<span style="color: #999999">Due: <?= $invoice['due_date'] ?></span>
<!-- Correct — #767676 on #fff is 4.54:1 (just passes) -->
<span style="color: #595959">Due: <?= $invoice['due_date'] ?></span>
Missing Form Labels (WCAG 1.3.1, 4.1.2)
Every form input must have a programmatically associated label. Placeholder text does not count as a label — it disappears when the user starts typing and is not reliably announced by screen readers. In PHP payment forms this is almost universally violated:
<!-- Violation — placeholder only, no label -->
<input type="text" name="card_name" placeholder="Name on card">
<!-- Correct — explicit label with for/id association -->
<label for="card_name">Name on card</label>
<input type="text" id="card_name" name="card_name"
autocomplete="cc-name" required>
<!-- If you must visually hide the label (e.g., search bars) -->
<label for="unit_search" class="sr-only">Search units</label>
<input type="search" id="unit_search" name="q">
The sr-only class is a standard CSS pattern that visually hides content while keeping it accessible to screen readers:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Keyboard Navigation Failures (WCAG 2.1.1)
Every interactive element must be operable by keyboard. Billing UIs frequently use <div> and <span> elements styled as buttons with JavaScript click handlers — these receive no keyboard focus by default:
<!-- Violation — div with click handler, not keyboard accessible -->
<div class="pay-btn" onclick="processPayment(<?= $invoice['id'] ?>)">
Pay Now
</div>
<!-- Correct — use a real button element -->
<button type="button"
class="pay-btn"
onclick="processPayment(<?= (int) $invoice['id'] ?>)"
data-invoice-id="<?= (int) $invoice['id'] ?>">
Pay Now
</button>
If you absolutely must use a non-semantic element for an interactive control, add tabindex="0", role="button", and a keydown handler for Enter and Space. Using a real <button> is simpler and correct.
Missing ARIA Labels on Dynamic Content (WCAG 4.1.2)
Dynamic regions in billing applications — payment status messages, balance updates, error notifications — need ARIA live regions so screen readers announce changes without requiring the user to navigate to them:
<!-- Payment status message area -->
<div id="payment-status"
role="status"
aria-live="polite"
aria-atomic="true">
<!-- PHP or JS populates this -->
</div>
<!-- Error messages use assertive for immediate announcement -->
<div id="payment-errors"
role="alert"
aria-live="assertive">
<?php if (!empty($errors)): ?>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
PHP Templating Patterns That Make Compliance Easier
The best accessibility approach at the framework level is building accessible components once and using them consistently. Create PHP helper functions for common patterns:
function formField(string $id, string $label, string $type = 'text', array $attrs = []): string
{
$attrStr = '';
foreach ($attrs as $k => $v) {
$attrStr .= ' ' . htmlspecialchars($k) . '="' . htmlspecialchars($v) . '"';
}
$required = isset($attrs['required']) ? '<span aria-hidden="true"> *</span>' : '';
return sprintf(
'<div class="field-group">
<label for="%s">%s%s</label>
<input type="%s" id="%s" name="%s"%s>
</div>',
htmlspecialchars($id),
htmlspecialchars($label),
$required,
htmlspecialchars($type),
htmlspecialchars($id),
htmlspecialchars($id),
$attrStr
);
}
// Usage in payment form template
echo formField('amount', 'Payment amount', 'number', [
'required' => '',
'min' => '1',
'step' => '0.01',
'autocomplete'=> 'off',
'aria-describedby' => 'amount-hint'
]);
Automated vs Manual Testing
Automated tools — axe DevTools, WAVE, Lighthouse accessibility audit — catch roughly 30–40% of WCAG violations. They're fast and should run on every deployment, but they cannot catch everything. Automated tools cannot determine whether alt text is meaningful (only that it exists), whether form error messages are clear to a screen reader user, or whether the keyboard navigation flow makes logical sense.
Manual testing requires: navigating your entire payment flow using only a keyboard (Tab, Shift+Tab, Enter, Space, arrow keys), running a screen reader (NVDA on Windows is free, VoiceOver is built into macOS and iOS) through the complete flow, and testing at 400% browser zoom for 1.4.4 compliance. Budget two to four hours for a thorough manual test of a billing portal.
Responding to a Demand Letter
If you receive an ADA demand letter targeting a site you built or maintain: do not ignore it, do not respond admitting liability without legal counsel, and do not assume it will go away. The firms sending these letters are organized and will file in federal court.
The practical response: engage a plaintiff's counsel-experienced ADA attorney (not your general business attorney), run an immediate accessibility audit to document current state, begin remediation and document every fix with dates and git commits, and propose a remediation timeline in your response. Courts and opposing counsel respond well to documented good-faith remediation in progress. The goal is a settlement that includes a remediation agreement rather than a judgment.
The most important thing is not to be the easiest target. Automated accessibility scanning is cheap and the demand letter business model depends on volume. Getting your WCAG 2.1 AA compliance to a reasonable baseline means your site doesn't show up in the scanner's hit list. The fixes for the most common violations — labels, alt text, color contrast, semantic HTML — are not expensive. They're the kind of thing you can build into your development process so you're not fixing them reactively under legal pressure.