typo3-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTYPO3 Accessibility (WCAG 2.2 AA)
TYPO3 无障碍适配(WCAG 2.2 AA标准)
Compatibility: TYPO3 v14.x (primary), v13.x, v12.4 LTS (fallbacks noted). All code works on v14. Version-specific fallbacks are marked with.v12/v13:
TYPO3 API First: Always use TYPO3's built-in APIs, Fluid ViewHelpers, and core features before adding custom markup. Verify methods exist in your target version.
PHP & JS over TypoScript: This skill provides PHP middleware, Fluid partials, and vanilla JavaScript solutions. TypoScript examples are avoided; use PHP-based approaches.
兼容性: TYPO3 v14.x(主要支持版本)、v13.x、v12.4 LTS(标注了兼容回退方案)。 所有代码均适用于v14版本,针对特定版本的回退方案会标记为。v12/v13:
TYPO3 API优先原则: 在添加自定义标记之前,务必优先使用TYPO3的内置API、Fluid ViewHelper和核心功能。请验证目标版本中是否存在对应方法。
优先使用PHP和JS而非TypoScript: 本方案提供PHP中间件、Fluid局部模板和原生JavaScript解决方案。避免使用TypoScript示例,采用基于PHP的实现方式。
1. Accessibility Checklist (Go-Live Gate)
1. 无障碍检查清单(上线准入标准)
Run through this checklist before every deployment. Mark items as you fix them.
每次部署前请逐一检查以下项,修复后标记完成状态。
Semantic Structure
语义化结构
- Every page has exactly one
<h1> - Heading hierarchy is sequential (>
h1>h2, no skips)h3 - Landmark elements used: ,
<main>,<nav>,<header>,<footer><aside> - Skip-to-content link is the first focusable element
- matches page language
<html lang="..."> - Language changes within content use attribute on containing element
lang
- 每个页面仅包含一个标签
<h1> - 标题层级连续(>
h1>h2,禁止跳过层级)h3 - 使用地标元素:、
<main>、<nav>、<header>、<footer><aside> - 跳转到内容的链接是第一个可获取焦点的元素
- 与页面语言匹配
<html lang="..."> - 内容中语言切换的部分,在包含元素上添加属性
lang
Images & Media
图片与媒体
- All have
<img>attribute (emptyaltfor decorative)alt="" - All have explicit
<img>andwidth(prevents CLS)height - Complex images (charts, infographics) have long description
- Videos have captions/subtitles
- Audio content has transcript
- No auto-playing media with sound
- 所有标签均包含
<img>属性(装饰性图片使用空值alt)alt="" - 所有标签均显式设置
<img>和width(避免布局偏移CLS)height - 复杂图片(图表、信息图)包含详细描述
- 视频添加字幕/副标题
- 音频内容提供文字转录
- 禁止自动播放带声音的媒体
Color & Contrast
色彩与对比度
- Text contrast >= 4.5:1 (normal) / 3:1 (large text >= 24px / 18.66px bold)
- UI components and graphical objects >= 3:1 contrast
- Information not conveyed by color alone (add icon, text, or pattern)
- Dark mode (if implemented) maintains contrast ratios
- 普通文本对比度≥4.5:1,大文本(≥24px或≥18.66px加粗)对比度≥3:1
- UI组件和图形对象对比度≥3:1
- 信息传递不依赖单一色彩(需搭配图标、文字或图案)
- 若实现深色模式,需维持符合标准的对比度
Keyboard & Focus
键盘与焦点
- All interactive elements reachable via Tab
- Focus order matches visual order
- Visible focus indicator on all interactive elements ()
:focus-visible - No focus traps (except modals — which must trap correctly)
- Escape closes modals/overlays and returns focus to trigger
- Custom widgets have correct keyboard handlers
- 所有交互元素均可通过Tab键访问
- 焦点顺序与视觉顺序一致
- 所有交互元素均有可见的焦点指示器(使用)
:focus-visible - 无焦点陷阱(模态框除外,但需正确实现焦点陷阱)
- 按Escape键可关闭模态框/浮层,并将焦点返回至触发元素
- 自定义组件具备正确的键盘事件处理逻辑
Forms
表单
- Every input has a programmatic (explicit
<label>/foror wrapping)id - Placeholder is not used as sole label
- Required fields indicated visually and with attribute
required - Error messages linked to inputs via +
aria-describedbyaria-invalid - Form groups use +
<fieldset><legend> - Correct and
typeattributes on inputsautocomplete
- 每个输入框均关联程序化的标签(使用显式
<label>/for或包裹方式)id - 禁止仅使用占位符作为标签
- 必填字段同时提供视觉标识和属性
required - 错误提示通过和
aria-describedby关联到对应输入框aria-invalid - 表单组使用+
<fieldset>结构<legend> - 输入框设置正确的和
type属性autocomplete
ARIA & Dynamic Content
ARIA与动态内容
- Icon-only buttons have
aria-label - Decorative icons have
aria-hidden="true" - Dynamic updates use or
aria-live="polite"role="status" - Error alerts use
role="alert" - Active navigation links have
aria-current="page" - Expandable elements use
aria-expanded
- 仅含图标的按钮添加属性
aria-label - 装饰性图标设置
aria-hidden="true" - 动态更新内容使用或
aria-live="polite"role="status" - 错误提示使用
role="alert" - 当前激活的导航链接设置
aria-current="page" - 可展开元素使用属性
aria-expanded
Motion & Interaction
动效与交互
- respected (CSS and JS)
prefers-reduced-motion - No content flashes more than 3 times per second
- Touch targets >= 44x44 CSS pixels
- is NOT set in viewport meta
user-scalable=no
- 尊重设置(CSS和JS层面均需适配)
prefers-reduced-motion - 内容闪烁频率不超过每秒3次
- 触摸目标尺寸≥44x44 CSS像素
- 禁止在视口meta标签中设置
user-scalable=no
TYPO3-Specific
TYPO3专属检查项
- Backend: alt text fields are filled for all images in File List
- Backend: content elements have descriptive headers (or for hidden)
header_layout = 100 - Backend: page properties have proper and
<title>description - Fluid templates use /
<f:link.page>(not<f:link.typolink>with manual hrefs)<a> - Content Block / Content Element templates follow accessible patterns below
- 后端:文件列表中所有图片均填写替代文本字段
- 后端:内容元素设置描述性标题(或设置隐藏标题)
header_layout = 100 - 后端:页面属性设置正确的和
<title>description - Fluid模板使用/
<f:link.page>(而非手动设置href的<f:link.typolink>标签)<a> - 内容块/内容元素模板遵循下方的无障碍设计模式
2. Fluid Template Patterns
2. Fluid模板设计模式
2.1 Page Layout with Landmarks
2.1 包含地标元素的页面布局
html
<!-- EXT:site_package/Resources/Private/Layouts/Default.html -->
<f:layout name="Default" />
<f:render section="Main" />html
<!-- EXT:site_package/Resources/Private/Templates/Default.html -->
<f:layout name="Default" />
<f:section name="Main">
<a href="#main-content" class="skip-link">
<f:translate key="LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:skip_to_content" />
</a>
<header role="banner">
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.main')}">
<f:cObject typoscriptObjectPath="lib.mainNavigation" />
</nav>
</header>
<main id="main-content">
<f:render section="Content" />
</main>
<footer>
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.footer')}">
<f:cObject typoscriptObjectPath="lib.footerNavigation" />
</nav>
</footer>
</f:section>html
<!-- EXT:site_package/Resources/Private/Layouts/Default.html -->
<f:layout name="Default" />
<f:render section="Main" />html
<!-- EXT:site_package/Resources/Private/Templates/Default.html -->
<f:layout name="Default" />
<f:section name="Main">
<a href="#main-content" class="skip-link">
<f:translate key="LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:skip_to_content" />
</a>
<header role="banner">
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.main')}">
<f:cObject typoscriptObjectPath="lib.mainNavigation" />
</nav>
</header>
<main id="main-content">
<f:render section="Content" />
</main>
<footer>
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.footer')}">
<f:cObject typoscriptObjectPath="lib.footerNavigation" />
</nav>
</footer>
</f:section>2.2 Skip Link CSS
2.2 跳转链接CSS样式
css
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 10000;
padding: 0.75rem 1.5rem;
background: var(--color-primary, #2563eb);
color: var(--color-on-primary, #fff);
font-weight: 600;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}css
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 10000;
padding: 0.75rem 1.5rem;
background: var(--color-primary, #2563eb);
color: var(--color-on-primary, #fff);
font-weight: 600;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}2.3 Accessible Image Rendering
2.3 无障碍图片渲染
html
<!-- Partial: Resources/Private/Partials/Media/Image.html -->
<f:if condition="{image}">
<figure>
<f:image
image="{image}"
alt="{image.alternative}"
title="{image.title}"
width="{dimensions.width}"
height="{dimensions.height}"
loading="{f:if(condition: '{lazyLoad}', then: 'lazy', else: 'eager')}"
/>
<f:if condition="{image.description}">
<figcaption>{image.description}</figcaption>
</f:if>
</figure>
</f:if>For decorative images (no informational value):
html
<f:image image="{image}" alt="" role="presentation" />html
<!-- Partial: Resources/Private/Partials/Media/Image.html -->
<f:if condition="{image}">
<figure>
<f:image
image="{image}"
alt="{image.alternative}"
title="{image.title}"
width="{dimensions.width}"
height="{dimensions.height}"
loading="{f:if(condition: '{lazyLoad}', then: 'lazy', else: 'eager')}"
/>
<f:if condition="{image.description}">
<figcaption>{image.description}</figcaption>
</f:if>
</figure>
</f:if>装饰性图片(无信息价值):
html
<f:image image="{image}" alt="" role="presentation" />2.4 Accessible Content Element Wrapper
2.4 无障碍内容元素包裹器
html
<!-- Partial: Resources/Private/Partials/ContentElement/Header.html -->
<f:if condition="{data.header} && {data.header_layout} != 100">
<f:switch expression="{data.header_layout}">
<f:case value="1"><h1>{data.header}</h1></f:case>
<f:case value="2"><h2>{data.header}</h2></f:case>
<f:case value="3"><h3>{data.header}</h3></f:case>
<f:case value="4"><h4>{data.header}</h4></f:case>
<f:case value="5"><h5>{data.header}</h5></f:case>
<f:defaultCase><h2>{data.header}</h2></f:defaultCase>
</f:switch>
</f:if>html
<!-- Partial: Resources/Private/Partials/ContentElement/Header.html -->
<f:if condition="{data.header} && {data.header_layout} != 100">
<f:switch expression="{data.header_layout}">
<f:case value="1"><h1>{data.header}</h1></f:case>
<f:case value="2"><h2>{data.header}</h2></f:case>
<f:case value="3"><h3>{data.header}</h3></f:case>
<f:case value="4"><h4>{data.header}</h4></f:case>
<f:case value="5"><h5>{data.header}</h5></f:case>
<f:defaultCase><h2>{data.header}</h2></f:defaultCase>
</f:switch>
</f:if>2.5 Accessible Navigation Partial
2.5 无障碍导航局部模板
html
<!-- Partial: Resources/Private/Partials/Navigation/MainMenu.html -->
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.main')}">
<ul role="list">
<f:for each="{menu}" as="item">
<li>
<f:if condition="{item.active}">
<f:then>
<a href="{item.link}" aria-current="page">{item.title}</a>
</f:then>
<f:else>
<a href="{item.link}">{item.title}</a>
</f:else>
</f:if>
<f:if condition="{item.children}">
<ul>
<f:for each="{item.children}" as="child">
<li>
<a href="{child.link}"
{f:if(condition: '{child.active}', then: 'aria-current="page"')}>
{child.title}
</a>
</li>
</f:for>
</ul>
</f:if>
</li>
</f:for>
</ul>
</nav>html
<!-- Partial: Resources/Private/Partials/Navigation/MainMenu.html -->
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.main')}">
<ul role="list">
<f:for each="{menu}" as="item">
<li>
<f:if condition="{item.active}">
<f:then>
<a href="{item.link}" aria-current="page">{item.title}</a>
</f:then>
<f:else>
<a href="{item.link}">{item.title}</a>
</f:else>
</f:if>
<f:if condition="{item.children}">
<ul>
<f:for each="{item.children}" as="child">
<li>
<a href="{child.link}"
{f:if(condition: '{child.active}', then: 'aria-current="page"')}>
{child.title}
</a>
</li>
</f:for>
</ul>
</f:if>
</li>
</f:for>
</ul>
</nav>2.6 Accessible Accordion (Content Blocks / Custom CE)
2.6 无障碍手风琴组件(内容块/自定义内容元素)
html
<div class="accordion" data-accordion>
<f:for each="{items}" as="item" iteration="iter">
<div class="accordion__item">
<h3>
<button
type="button"
class="accordion__trigger"
aria-expanded="false"
aria-controls="accordion-panel-{data.uid}-{iter.index}"
id="accordion-header-{data.uid}-{iter.index}"
data-accordion-trigger
>
{item.header}
</button>
</h3>
<div
id="accordion-panel-{data.uid}-{iter.index}"
role="region"
aria-labelledby="accordion-header-{data.uid}-{iter.index}"
class="accordion__panel"
hidden
>
<f:format.html>{item.bodytext}</f:format.html>
</div>
</div>
</f:for>
</div>html
<div class="accordion" data-accordion>
<f:for each="{items}" as="item" iteration="iter">
<div class="accordion__item">
<h3>
<button
type="button"
class="accordion__trigger"
aria-expanded="false"
aria-controls="accordion-panel-{data.uid}-{iter.index}"
id="accordion-header-{data.uid}-{iter.index}"
data-accordion-trigger
>
{item.header}
</button>
</h3>
<div
id="accordion-panel-{data.uid}-{iter.index}"
role="region"
aria-labelledby="accordion-header-{data.uid}-{iter.index}"
class="accordion__panel"
hidden
>
<f:format.html>{item.bodytext}</f:format.html>
</div>
</div>
</f:for>
</div>2.7 Accessible Tab Component
2.7 无障碍标签页组件
html
<div class="tabs" data-tabs>
<div role="tablist" aria-label="{f:translate(key: 'tabs.label')}">
<f:for each="{items}" as="item" iteration="iter">
<button
type="button"
role="tab"
id="tab-{data.uid}-{iter.index}"
aria-controls="tabpanel-{data.uid}-{iter.index}"
aria-selected="{f:if(condition: '{iter.isFirst}', then: 'true', else: 'false')}"
tabindex="{f:if(condition: '{iter.isFirst}', then: '0', else: '-1')}"
>
{item.header}
</button>
</f:for>
</div>
<f:for each="{items}" as="item" iteration="iter">
<div
role="tabpanel"
id="tabpanel-{data.uid}-{iter.index}"
aria-labelledby="tab-{data.uid}-{iter.index}"
tabindex="0"
{f:if(condition: '!{iter.isFirst}', then: 'hidden')}
>
<f:format.html>{item.bodytext}</f:format.html>
</div>
</f:for>
</div>html
<div class="tabs" data-tabs>
<div role="tablist" aria-label="{f:translate(key: 'tabs.label')}">
<f:for each="{items}" as="item" iteration="iter">
<button
type="button"
role="tab"
id="tab-{data.uid}-{iter.index}"
aria-controls="tabpanel-{data.uid}-{iter.index}"
aria-selected="{f:if(condition: '{iter.isFirst}', then: 'true', else: 'false')}"
tabindex="{f:if(condition: '{iter.isFirst}', then: '0', else: '-1')}"
>
{item.header}
</button>
</f:for>
</div>
<f:for each="{items}" as="item" iteration="iter">
<div
role="tabpanel"
id="tabpanel-{data.uid}-{iter.index}"
aria-labelledby="tab-{data.uid}-{iter.index}"
tabindex="0"
{f:if(condition: '!{iter.isFirst}', then: 'hidden')}
>
<f:format.html>{item.bodytext}</f:format.html>
</div>
</f:for>
</div>3. PHP: Accessibility Middleware & Helpers
3. PHP:无障碍中间件与工具类
3.1 Language Attribute Middleware (PSR-15)
3.1 语言属性中间件(PSR-15)
Ensures is always correct based on site language config.
<html lang="...">php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Attribute\AsMiddleware;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
#[AsMiddleware(
identifier: 'vendor/site-package/accessibility-lang',
after: 'typo3/cms-frontend/content-length-headers',
)]
final class AccessibilityLangMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$siteLanguage = $request->getAttribute('language');
if (!$siteLanguage instanceof SiteLanguage) {
return $response;
}
$contentType = $response->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'text/html')) {
return $response;
}
$locale = $siteLanguage->getLocale();
$langCode = $locale->getLanguageCode();
$body = (string)$response->getBody();
$body = preg_replace(
'/<html([^>]*)lang="[^"]*"/',
'<html$1lang="' . htmlspecialchars($langCode) . '"',
$body,
1
);
$response->getBody()->rewind();
$response->getBody()->write($body);
return $response;
}
}v12/v13 fallback: Register via instead of the attribute:
Configuration/RequestMiddlewares.php#[AsMiddleware]php
<?php
// Configuration/RequestMiddlewares.php (v12/v13)
return [
'frontend' => [
'vendor/site-package/accessibility-lang' => [
'target' => \Vendor\SitePackage\Middleware\AccessibilityLangMiddleware::class,
'after' => ['typo3/cms-frontend/content-length-headers'],
],
],
];确保始终与站点语言配置一致。
<html lang="...">php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Attribute\AsMiddleware;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
#[AsMiddleware(
identifier: 'vendor/site-package/accessibility-lang',
after: 'typo3/cms-frontend/content-length-headers',
)]
final class AccessibilityLangMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$siteLanguage = $request->getAttribute('language');
if (!$siteLanguage instanceof SiteLanguage) {
return $response;
}
$contentType = $response->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'text/html')) {
return $response;
}
$locale = $siteLanguage->getLocale();
$langCode = $locale->getLanguageCode();
$body = (string)$response->getBody();
$body = preg_replace(
'/<html([^>]*)lang="[^"]*"/',
'<html$1lang="' . htmlspecialchars($langCode) . '"',
$body,
1
);
$response->getBody()->rewind();
$response->getBody()->write($body);
return $response;
}
}v12/v13回退方案: 不使用属性,而是通过注册:
#[AsMiddleware]Configuration/RequestMiddlewares.phpphp
<?php
// Configuration/RequestMiddlewares.php (v12/v13)
return [
'frontend' => [
'vendor/site-package/accessibility-lang' => [
'target' => \Vendor\SitePackage\Middleware\AccessibilityLangMiddleware::class,
'after' => ['typo3/cms-frontend/content-length-headers'],
],
],
];3.2 Image Alt Text Validation (PSR-14 Event)
3.2 图片替代文本验证(PSR-14事件)
Warn editors when images lack alt text:
php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[AsEventListener(identifier: 'vendor/site-package/alt-text-check')]
final class ImageAltTextValidationListener
{
public function __construct(
private readonly FlashMessageService $flashMessageService,
) {}
public function __invoke(\TYPO3\CMS\Core\DataHandling\Event\AfterRecordPublishedEvent|object $event): void
{
// For DataHandler processDatamap_afterDatabaseOperations hook style usage:
// This can be adapted as a DataHandler hook or PSR-14 event depending on version.
}
public function processDatamap_afterDatabaseOperations(
string $status,
string $table,
string|int $id,
array $fieldArray,
DataHandler $dataHandler,
): void {
if ($table !== 'sys_file_metadata') {
return;
}
if (empty($fieldArray['alternative'] ?? '')) {
$message = GeneralUtility::makeInstance(
FlashMessage::class,
'Image is missing alt text. Please add descriptive alt text for accessibility (WCAG 1.1.1).',
'Accessibility Warning',
ContextualFeedbackSeverity::WARNING,
true,
);
$this->flashMessageService
->getMessageQueueByIdentifier()
->addMessage($message);
}
}
}Register as DataHandler hook in :
ext_localconf.phpphp
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= \Vendor\SitePackage\EventListener\ImageAltTextValidationListener::class;当图片缺少替代文本时,向编辑器发出警告:
php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[AsEventListener(identifier: 'vendor/site-package/alt-text-check')]
final class ImageAltTextValidationListener
{
public function __construct(
private readonly FlashMessageService $flashMessageService,
) {}
public function __invoke(\TYPO3\CMS\Core\DataHandling\Event\AfterRecordPublishedEvent|object $event): void
{
// 适配DataHandler的processDatamap_afterDatabaseOperations钩子用法:
// 可根据版本调整为DataHandler钩子或PSR-14事件
}
public function processDatamap_afterDatabaseOperations(
string $status,
string $table,
string|int $id,
array $fieldArray,
DataHandler $dataHandler,
): void {
if ($table !== 'sys_file_metadata') {
return;
}
if (empty($fieldArray['alternative'] ?? '')) {
$message = GeneralUtility::makeInstance(
FlashMessage::class,
'图片缺少替代文本,请添加描述性替代文本以符合无障碍标准(WCAG 1.1.1)。',
'无障碍警告',
ContextualFeedbackSeverity::WARNING,
true,
);
$this->flashMessageService
->getMessageQueueByIdentifier()
->addMessage($message);
}
}
}在中注册为DataHandler钩子:
ext_localconf.phpphp
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= \Vendor\SitePackage\EventListener\ImageAltTextValidationListener::class;3.3 Accessible Fluid ViewHelper: SrOnly
3.3 无障碍Fluid ViewHelper:SrOnly
Renders screen-reader-only text:
php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\ViewHelpers;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
final class SrOnlyViewHelper extends AbstractTagBasedViewHelper
{
protected $tagName = 'span';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerUniversalTagAttributes();
}
public function render(): string
{
$this->tag->addAttribute('class', 'sr-only');
$this->tag->setContent($this->renderChildren());
return $this->tag->render();
}
}css
/* CSS for sr-only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}Usage in Fluid:
html
{namespace sp = Vendor\SitePackage\ViewHelpers}
<button aria-label="{f:translate(key: 'button.close')}">
<svg aria-hidden="true"><!-- icon --></svg>
<sp:srOnly><f:translate key="button.close" /></sp:srOnly>
</button>仅为屏幕阅读器渲染文本:
php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\ViewHelpers;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
final class SrOnlyViewHelper extends AbstractTagBasedViewHelper
{
protected $tagName = 'span';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerUniversalTagAttributes();
}
public function render(): string
{
$this->tag->addAttribute('class', 'sr-only');
$this->tag->setContent($this->renderChildren());
return $this->tag->render();
}
}css
/* SrOnly样式的CSS */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}Fluid中的用法:
html
{namespace sp = Vendor\SitePackage\ViewHelpers}
<button aria-label="{f:translate(key: 'button.close')}">
<svg aria-hidden="true"><!-- 图标 --></svg>
<sp:srOnly><f:translate key="button.close" /></sp:srOnly>
</button>4. JavaScript: Accessible Widgets
4. JavaScript:无障碍组件
4.1 Accordion
4.1 手风琴组件
javascript
// EXT:site_package/Resources/Public/JavaScript/accordion.js
class AccessibleAccordion {
constructor(container) {
this.container = container;
this.triggers = container.querySelectorAll('[data-accordion-trigger]');
this.init();
}
init() {
this.triggers.forEach((trigger) => {
trigger.addEventListener('click', () => this.toggle(trigger));
trigger.addEventListener('keydown', (e) => this.handleKeydown(e));
});
}
toggle(trigger) {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const panelId = trigger.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
trigger.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
}
handleKeydown(e) {
const triggers = [...this.triggers];
const index = triggers.indexOf(e.currentTarget);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
triggers[(index + 1) % triggers.length].focus();
break;
case 'ArrowUp':
e.preventDefault();
triggers[(index - 1 + triggers.length) % triggers.length].focus();
break;
case 'Home':
e.preventDefault();
triggers[0].focus();
break;
case 'End':
e.preventDefault();
triggers[triggers.length - 1].focus();
break;
}
}
}
document.querySelectorAll('[data-accordion]').forEach((el) => new AccessibleAccordion(el));javascript
// EXT:site_package/Resources/Public/JavaScript/accordion.js
class AccessibleAccordion {
constructor(container) {
this.container = container;
this.triggers = container.querySelectorAll('[data-accordion-trigger]');
this.init();
}
init() {
this.triggers.forEach((trigger) => {
trigger.addEventListener('click', () => this.toggle(trigger));
trigger.addEventListener('keydown', (e) => this.handleKeydown(e));
});
}
toggle(trigger) {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const panelId = trigger.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
trigger.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
}
handleKeydown(e) {
const triggers = [...this.triggers];
const index = triggers.indexOf(e.currentTarget);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
triggers[(index + 1) % triggers.length].focus();
break;
case 'ArrowUp':
e.preventDefault();
triggers[(index - 1 + triggers.length) % triggers.length].focus();
break;
case 'Home':
e.preventDefault();
triggers[0].focus();
break;
case 'End':
e.preventDefault();
triggers[triggers.length - 1].focus();
break;
}
}
}
document.querySelectorAll('[data-accordion]').forEach((el) => new AccessibleAccordion(el));4.2 Tabs
4.2 标签页组件
javascript
// EXT:site_package/Resources/Public/JavaScript/tabs.js
class AccessibleTabs {
constructor(container) {
this.tablist = container.querySelector('[role="tablist"]');
this.tabs = [...this.tablist.querySelectorAll('[role="tab"]')];
this.panels = this.tabs.map(
(tab) => document.getElementById(tab.getAttribute('aria-controls'))
);
this.init();
}
init() {
this.tabs.forEach((tab) => {
tab.addEventListener('click', () => this.selectTab(tab));
tab.addEventListener('keydown', (e) => this.handleKeydown(e));
});
}
selectTab(selectedTab) {
this.tabs.forEach((tab, i) => {
const selected = tab === selectedTab;
tab.setAttribute('aria-selected', String(selected));
tab.tabIndex = selected ? 0 : -1;
this.panels[i].hidden = !selected;
});
selectedTab.focus();
}
handleKeydown(e) {
const index = this.tabs.indexOf(e.currentTarget);
let next;
switch (e.key) {
case 'ArrowRight':
next = (index + 1) % this.tabs.length;
break;
case 'ArrowLeft':
next = (index - 1 + this.tabs.length) % this.tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = this.tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
this.selectTab(this.tabs[next]);
}
}
document.querySelectorAll('[data-tabs]').forEach((el) => new AccessibleTabs(el));javascript
// EXT:site_package/Resources/Public/JavaScript/tabs.js
class AccessibleTabs {
constructor(container) {
this.tablist = container.querySelector('[role="tablist"]');
this.tabs = [...this.tablist.querySelectorAll('[role="tab"]')];
this.panels = this.tabs.map(
(tab) => document.getElementById(tab.getAttribute('aria-controls'))
);
this.init();
}
init() {
this.tabs.forEach((tab) => {
tab.addEventListener('click', () => this.selectTab(tab));
tab.addEventListener('keydown', (e) => this.handleKeydown(e));
});
}
selectTab(selectedTab) {
this.tabs.forEach((tab, i) => {
const selected = tab === selectedTab;
tab.setAttribute('aria-selected', String(selected));
tab.tabIndex = selected ? 0 : -1;
this.panels[i].hidden = !selected;
});
selectedTab.focus();
}
handleKeydown(e) {
const index = this.tabs.indexOf(e.currentTarget);
let next;
switch (e.key) {
case 'ArrowRight':
next = (index + 1) % this.tabs.length;
break;
case 'ArrowLeft':
next = (index - 1 + this.tabs.length) % this.tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = this.tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
this.selectTab(this.tabs[next]);
}
}
document.querySelectorAll('[data-tabs]').forEach((el) => new AccessibleTabs(el));4.3 Modal / Dialog
4.3 模态框/对话框
javascript
// EXT:site_package/Resources/Public/JavaScript/dialog.js
class AccessibleDialog {
constructor(dialog) {
this.dialog = dialog;
this.previousFocus = null;
this.init();
}
init() {
this.dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close();
if (e.key === 'Tab') this.trapFocus(e);
});
this.dialog.addEventListener('click', (e) => {
if (e.target === this.dialog) this.close();
});
}
open() {
this.previousFocus = document.activeElement;
this.dialog.showModal();
const firstFocusable = this.dialog.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
}
close() {
this.dialog.close();
this.previousFocus?.focus();
}
trapFocus(e) {
const focusable = this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}Use the native element in Fluid:
<dialog>html
<dialog id="my-dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">{dialogTitle}</h2>
<div>{dialogContent}</div>
<button type="button" data-close-dialog>
<f:translate key="button.close" />
</button>
</dialog>javascript
// EXT:site_package/Resources/Public/JavaScript/dialog.js
class AccessibleDialog {
constructor(dialog) {
this.dialog = dialog;
this.previousFocus = null;
this.init();
}
init() {
this.dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close();
if (e.key === 'Tab') this.trapFocus(e);
});
this.dialog.addEventListener('click', (e) => {
if (e.target === this.dialog) this.close();
});
}
open() {
this.previousFocus = document.activeElement;
this.dialog.showModal();
const firstFocusable = this.dialog.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
}
close() {
this.dialog.close();
this.previousFocus?.focus();
}
trapFocus(e) {
const focusable = this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}在Fluid中使用原生元素:
<dialog>html
<dialog id="my-dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">{dialogTitle}</h2>
<div>{dialogContent}</div>
<button type="button" data-close-dialog>
<f:translate key="button.close" />
</button>
</dialog>4.4 Reduced Motion Check
4.4 减少动效检测
javascript
// EXT:site_package/Resources/Public/JavaScript/motion.js
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function getTransitionDuration() {
return prefersReducedMotion.matches ? 0 : 300;
}
prefersReducedMotion.addEventListener('change', () => {
document.documentElement.classList.toggle('reduce-motion', prefersReducedMotion.matches);
});
if (prefersReducedMotion.matches) {
document.documentElement.classList.add('reduce-motion');
}css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}javascript
// EXT:site_package/Resources/Public/JavaScript/motion.js
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function getTransitionDuration() {
return prefersReducedMotion.matches ? 0 : 300;
}
prefersReducedMotion.addEventListener('change', () => {
document.documentElement.classList.toggle('reduce-motion', prefersReducedMotion.matches);
});
if (prefersReducedMotion.matches) {
document.documentElement.classList.add('reduce-motion');
}css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}4.5 Live Region Announcer
4.5 实时区域播报器
javascript
// EXT:site_package/Resources/Public/JavaScript/announcer.js
class LiveAnnouncer {
constructor() {
this.region = document.createElement('div');
this.region.setAttribute('aria-live', 'polite');
this.region.setAttribute('aria-atomic', 'true');
this.region.classList.add('sr-only');
document.body.appendChild(this.region);
}
announce(message, priority = 'polite') {
this.region.setAttribute('aria-live', priority);
this.region.textContent = '';
requestAnimationFrame(() => {
this.region.textContent = message;
});
}
}
window.liveAnnouncer = new LiveAnnouncer();Usage:
window.liveAnnouncer.announce('3 results found');javascript
// EXT:site_package/Resources/Public/JavaScript/announcer.js
class LiveAnnouncer {
constructor() {
this.region = document.createElement('div');
this.region.setAttribute('aria-live', 'polite');
this.region.setAttribute('aria-atomic', 'true');
this.region.classList.add('sr-only');
document.body.appendChild(this.region);
}
announce(message, priority = 'polite') {
this.region.setAttribute('aria-live', priority);
this.region.textContent = '';
requestAnimationFrame(() => {
this.region.textContent = message;
});
}
}
window.liveAnnouncer = new LiveAnnouncer();用法:
window.liveAnnouncer.announce('找到3条结果');5. CSS: Focus & Contrast Essentials
5. CSS:焦点与对比度核心样式
css
/* EXT:site_package/Resources/Public/Css/accessibility.css */
/* Visible focus indicator */
:focus-visible {
outline: 3px solid var(--focus-color, #2563eb);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
/* Touch targets */
button, a, input, select, textarea, [role="button"] {
min-height: 44px;
min-width: 44px;
}
/* Ensure readable line height (WCAG 1.4.12) */
body {
line-height: 1.6;
}
p + p {
margin-top: 1em;
}
/* Max line length for readability */
.ce-bodytext, .frame-default .content {
max-width: 75ch;
}
/* Scroll margin for anchored headings */
[id] {
scroll-margin-top: 5rem;
}
/* Hover + focus parity */
a:hover, a:focus-visible,
button:hover, button:focus-visible {
text-decoration: underline;
}css
/* EXT:site_package/Resources/Public/Css/accessibility.css */
/* 可见的焦点指示器 */
:focus-visible {
outline: 3px solid var(--focus-color, #2563eb);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
/* 触摸目标尺寸 */
button, a, input, select, textarea, [role="button"] {
min-height: 44px;
min-width: 44px;
}
/* 确保行高可读性(WCAG 1.4.12) */
body {
line-height: 1.6;
}
p + p {
margin-top: 1em;
}
/* 最大行宽优化可读性 */
.ce-bodytext, .frame-default .content {
max-width: 75ch;
}
/* 锚点标题的滚动边距 */
[id] {
scroll-margin-top: 5rem;
}
/* 悬停与焦点样式一致性 */
a:hover, a:focus-visible,
button:hover, button:focus-visible {
text-decoration: underline;
}6. TYPO3-Specific Configuration
6. TYPO3专属配置
6.1 TCA: Require Alt Text on Images
6.1 TCA:强制图片添加替代文本
Make the alt text field required in file metadata:
php
<?php
// Configuration/TCA/Overrides/sys_file_metadata.php
$GLOBALS['TCA']['sys_file_metadata']['columns']['alternative']['config']['required'] = true;
// v12/v13 fallback: use eval instead
// $GLOBALS['TCA']['sys_file_metadata']['columns']['alternative']['config']['eval'] = 'required';在文件元数据中设置替代文本为必填项:
php
<?php
// Configuration/TCA/Overrides/sys_file_metadata.php
$GLOBALS['TCA']['sys_file_metadata']['columns']['alternative']['config']['required'] = true;
// v12/v13回退方案:使用eval替代
// $GLOBALS['TCA']['sys_file_metadata']['columns']['alternative']['config']['eval'] = 'required';6.2 TCA: Header Layout Options
6.2 TCA:标题层级选项
Provide proper heading levels and a "hidden" option for content elements:
php
<?php
// Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['items'] = [
['label' => 'H1', 'value' => '1'],
['label' => 'H2', 'value' => '2'],
['label' => 'H3', 'value' => '3'],
['label' => 'H4', 'value' => '4'],
['label' => 'H5', 'value' => '5'],
['label' => 'Hidden', 'value' => '100'],
];
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['default'] = '2';v12 fallback: Use numeric array keys = label, = value:
[0][1]php
// v12 style
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['items'] = [
['H1', '1'],
['H2', '2'],
['H3', '3'],
['H4', '4'],
['H5', '5'],
['Hidden', '100'],
];为内容元素提供合理的标题层级和“隐藏”选项:
php
<?php
// Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['items'] = [
['label' => 'H1', 'value' => '1'],
['label' => 'H2', 'value' => '2'],
['label' => 'H3', 'value' => '3'],
['label' => 'H4', 'value' => '4'],
['label' => 'H5', 'value' => '5'],
['label' => '隐藏', 'value' => '100'],
];
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['default'] = '2';v12回退方案: 使用数字数组键,为标签,为值:
[0][1]php
// v12格式
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['items'] = [
['H1', '1'],
['H2', '2'],
['H3', '3'],
['H4', '4'],
['H5', '5'],
['隐藏', '100'],
];6.3 Form Framework: Accessible Forms (EXT:form)
6.3 表单框架:无障碍表单(EXT:form)
yaml
undefinedyaml
undefinedEXT:site_package/Configuration/Form/Overrides/AccessibleForm.yaml
EXT:site_package/Configuration/Form/Overrides/AccessibleForm.yaml
TYPO3:
CMS:
Form:
prototypes:
standard:
formElementsDefinition:
Text:
properties:
fluidAdditionalAttributes:
autocomplete: 'on'
Email:
properties:
fluidAdditionalAttributes:
autocomplete: 'email'
inputmode: 'email'
Telephone:
properties:
fluidAdditionalAttributes:
autocomplete: 'tel'
inputmode: 'tel'
undefinedTYPO3:
CMS:
Form:
prototypes:
standard:
formElementsDefinition:
Text:
properties:
fluidAdditionalAttributes:
autocomplete: 'on'
Email:
properties:
fluidAdditionalAttributes:
autocomplete: 'email'
inputmode: 'email'
Telephone:
properties:
fluidAdditionalAttributes:
autocomplete: 'tel'
inputmode: 'tel'
undefined6.4 Content Security Policy: Allow Inline Focus Styles
6.4 内容安全策略:允许内联焦点样式
If CSP is enabled, ensure focus styles via external CSS (not inline):
php
<?php
// No inline styles needed — use the external accessibility.css
// If you must add inline styles, extend CSP via PSR-14:
declare(strict_types=1);
namespace Vendor\SitePackage\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashValue;
#[AsEventListener(identifier: 'vendor/site-package/csp-accessibility')]
final class CspAccessibilityListener
{
public function __invoke(PolicyMutatedEvent $event): void
{
if ($event->getScope()->type->isFrontend()) {
// Prefer external CSS over extending CSP for inline styles
}
}
}若启用CSP,请确保通过外部CSS实现焦点样式(而非内联样式):
php
<?php
// 无需内联样式 — 使用外部accessibility.css即可
// 若必须添加内联样式,可通过PSR-14扩展CSP:
declare(strict_types=1);
namespace Vendor\SitePackage\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashValue;
#[AsEventListener(identifier: 'vendor/site-package/csp-accessibility')]
final class CspAccessibilityListener
{
public function __invoke(PolicyMutatedEvent $event): void
{
if ($event->getScope()->type->isFrontend()) {
// 优先使用外部CSS,而非扩展CSP支持内联样式
}
}
}7. Testing & Tools
7. 测试与工具
Automated Testing
自动化测试
| Tool | Use |
|---|---|
| Browser extension for automated WCAG checks |
| CLI accessibility testing for CI/CD |
| Chrome DevTools audit (Accessibility score) |
| Browser extension for visual feedback |
| 工具 | 用途 |
|---|---|
| 浏览器扩展,用于自动化WCAG合规性检查 |
| CLI工具,用于CI/CD流程中的无障碍测试 |
| Chrome开发者工具中的审计功能(包含无障碍评分) |
| 浏览器扩展,提供可视化的无障碍问题反馈 |
Manual Testing Checklist
手动测试检查清单
- Keyboard only: Navigate entire page using Tab, Shift+Tab, Enter, Escape, Arrow keys
- Screen reader: Test with VoiceOver (macOS), NVDA/JAWS (Windows)
- Zoom: Verify layout at 200% and 400% zoom
- Color: Use browser dev tools to simulate color blindness
- Reduced motion: Enable in OS settings
prefers-reduced-motion: reduce
- 仅使用键盘:仅通过Tab、Shift+Tab、Enter、Escape、方向键导航整个页面
- 屏幕阅读器:使用VoiceOver(macOS)、NVDA/JAWS(Windows)进行测试
- 缩放测试:验证页面在200%和400%缩放比例下的布局是否正常
- 色彩模拟:使用浏览器开发者工具模拟色盲模式
- 减少动效:在系统设置中启用
prefers-reduced-motion: reduce
Pa11y CI Integration
Pa11y CI集成
bash
undefinedbash
undefinedRun against local DDEV instance
针对本地DDEV实例运行测试
npx pa11y https://mysite.ddev.site --standard WCAG2AA --reporter cli
```json
// .pa11yci.json
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 30000,
"wait": 2000
},
"urls": [
"https://mysite.ddev.site/",
"https://mysite.ddev.site/contact",
"https://mysite.ddev.site/news"
]
}npx pa11y https://mysite.ddev.site --standard WCAG2AA --reporter cli
```json
// .pa11yci.json
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 30000,
"wait": 2000
},
"urls": [
"https://mysite.ddev.site/",
"https://mysite.ddev.site/contact",
"https://mysite.ddev.site/news"
]
}8. Accessibility Extensions
8. 无障碍扩展推荐
| Extension | Purpose | v12 | v13 | v14 |
|---|---|---|---|---|
| Built-in ARIA, focus management | ✓ | ✓ | ✓ |
| Structured data (WebPage, Article) | ✓ | ✓ | ✓ |
| Accessibility toolbar for frontend | ✓ | ✓ | — |
| Accessible grid/container layouts | ✓ | ✓ | ✓ |
| 扩展 | 用途 | v12 | v13 | v14 |
|---|---|---|---|---|
| 内置ARIA支持、焦点管理 | ✓ | ✓ | ✓ |
| 结构化数据(WebPage、Article) | ✓ | ✓ | ✓ |
| 前端无障碍工具栏 | ✓ | ✓ | — |
| 无障碍网格/容器布局 | ✓ | ✓ | ✓ |
9. WCAG 2.2 Quick Reference
9. WCAG 2.2速查指南
| Success Criterion | Level | TYPO3 Solution |
|---|---|---|
| 1.1.1 Non-text Content | A | |
| 1.3.1 Info and Relationships | A | Semantic HTML in Fluid, |
| 1.3.5 Identify Input Purpose | AA | |
| 1.4.1 Use of Color | A | Icons + text alongside color indicators |
| 1.4.3 Contrast (Minimum) | AA | CSS variables with 4.5:1 ratio |
| 1.4.4 Resize Text | AA | |
| 1.4.10 Reflow | AA | Fluid/responsive layouts |
| 1.4.11 Non-text Contrast | AA | 3:1 for UI components |
| 1.4.12 Text Spacing | AA | |
| 2.1.1 Keyboard | A | Native elements, keyboard handlers |
| 2.4.1 Bypass Blocks | A | Skip link |
| 2.4.3 Focus Order | A | Logical DOM order |
| 2.4.7 Focus Visible | AA | |
| 2.4.11 Focus Not Obscured | AA | |
| 2.5.8 Target Size | AA | 44x44px minimum |
| 3.1.1 Language of Page | A | |
| 3.1.2 Language of Parts | AA | |
| 3.3.1 Error Identification | A | |
| 3.3.2 Labels or Instructions | A | |
| 4.1.2 Name, Role, Value | A | ARIA attributes on custom widgets |
| 4.1.3 Status Messages | AA | |
| 成功标准 | 级别 | TYPO3实现方案 |
|---|---|---|
| 1.1.1 非文本内容 | A | |
| 1.3.1 信息与关系 | A | Fluid中使用语义化HTML, |
| 1.3.5 识别输入用途 | AA | 表单字段添加 |
| 1.4.1 色彩使用 | A | 色彩标识搭配图标+文字 |
| 1.4.3 最低对比度 | AA | 使用对比度≥4.5:1的CSS变量 |
| 1.4.4 文本缩放 | AA | 使用 |
| 1.4.10 重排 | AA | 流式/响应式布局 |
| 1.4.11 非文本对比度 | AA | UI组件对比度≥3:1 |
| 1.4.12 文本间距 | AA | 设置 |
| 2.1.1 键盘可达性 | A | 使用原生元素,实现键盘事件处理 |
| 2.4.1 绕过区块 | A | 添加跳转链接 |
| 2.4.3 焦点顺序 | A | 符合逻辑的DOM顺序 |
| 2.4.7 可见焦点 | AA | 使用 |
| 2.4.11 焦点不被遮挡 | AA | 设置 |
| 2.5.8 目标尺寸 | AA | 触摸目标≥44x44px |
| 3.1.1 页面语言 | A | 通过中间件设置 |
| 3.1.2 局部语言 | AA | 多语言内容添加 |
| 3.3.1 错误识别 | A | 使用 |
| 3.3.2 标签或说明 | A | 每个输入框关联 |
| 4.1.2 名称、角色、值 | A | 自定义组件添加ARIA属性 |
| 4.1.3 状态消息 | AA | 使用 |
10. Anti-Patterns (Flag These in Reviews)
10. 反模式(代码评审中需标记)
| Anti-Pattern | Fix |
|---|---|
| Use |
| Use |
| Add |
| Use |
| Remove it |
| Use |
Images without | Add |
| Color-only error indication | Add icon + text |
| Use |
Missing | Set via middleware or Fluid layout |
| Auto-playing video with sound | Add |
| Skipped heading levels | Fix hierarchy |
| 反模式 | 修复方案 |
|---|---|
| 替换为 |
| 使用 |
| 仅使用占位符作为标签 | 添加 |
| 使用 |
视口中设置 | 删除该设置 |
使用绝对单位 | 改用 |
图片无 | 添加 |
| 仅依赖色彩传递错误信息 | 添加图标+文字提示 |
无 | 替换为 |
| 通过中间件或Fluid布局设置 |
| 自动播放带声音的视频 | 添加 |
| 标题层级跳过 | 修复为连续层级 |
Related Skills
相关技能
- web-design-guidelines - General web accessibility audit (non-TYPO3)
- web-platform-design - WCAG 2.2, responsive design, forms
- typo3-seo - SEO and structured data (overlaps with a11y for alt text, headings)
- typo3-content-blocks - Accessible Content Element templates
- web-design-guidelines - 通用网页无障碍审计(非TYPO3专属)
- web-platform-design - WCAG 2.2、响应式设计、表单适配
- typo3-seo - SEO与结构化数据(与无障碍在替代文本、标题等方面有重叠)
- typo3-content-blocks - 无障碍内容元素模板
Credits & Attribution
致谢与引用
Accessibility patterns adapted from:
- Vercel Web Interface Guidelines (MIT License)
- Vercel Agent Skills: web-design-guidelines (MIT License)
- Platform Design Skills: web-platform-design (MIT License)
- W3C WAI-ARIA Authoring Practices
- WCAG 2.2 Specification
Thanks to Netresearch DTT GmbH for their contributions to the TYPO3 community.
无障碍设计模式改编自:
- Vercel Web Interface Guidelines(MIT许可证)
- Vercel Agent Skills: web-design-guidelines(MIT许可证)
- Platform Design Skills: web-platform-design(MIT许可证)
- W3C WAI-ARIA Authoring Practices
- WCAG 2.2 Specification
感谢Netresearch DTT GmbH为TYPO3社区做出的贡献。