Token导航 LogoToken导航TokenDH.com
前端设计需要联网github未标认证来源可访问许可证需确认审计通过

typo3-accessibilitytype3 辅助功能

Agent Skill

用于辅助无障碍访问检查、页面可用性审计和前端可访问性改进。它适合让 Agent 检查语义标签、键盘操作、颜色对比、ARIA 属性和自动化检测结果。使用时需要结合真实页面和浏览器验证,不应只依赖静态文本判断;涉及修复建议时,应兼顾设计系统、组件复用和 WCAG 等通用无障碍规范。

总安装

635

周安装

27

GitHub Stars

26

下载量

222
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:typo3-accessibility(type3 辅助功能)
来源仓库:https://github.com/dirnbauer/webconsulting-skills
仓库路径:skills/typo3-accessibility
安装命令:
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill typo3-accessibility
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill typo3-accessibility

简介

辅助无障碍访问检查与页面可用性审计,支持语义标签、键盘操作验证。

  • 可用于 ARIA 属性检查和 WCAG 规范符合性评估。
  • 需结合真实页面和浏览器工具验证结果准确性。
  • 修复建议应兼顾设计系统与通用无障碍规范要求。
  • typo3-accessibility 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

TYPO3 Accessibility (WCAG 2.2 AA)

Compatibility: TYPO3 v14.x only. Older cores are out of scope for this collection.
TYPO3 API First: Always use TYPO3's built-in APIs, Fluid ViewHelpers, and core features before adding custom markup. Verify methods exist in TYPO3 v14.
PHP & JS over TypoScript: This skill provides PHP middleware, Fluid partials, and vanilla JavaScript solutions. TypoScript examples are avoided; use PHP-based approaches.

1. Accessibility Checklist (Go-Live Gate)

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 > h3, no skips)
  • Landmark elements used: <main>, <nav>, <header>, <footer>, <aside>
  • Skip-to-content link is the first focusable element
  • <html lang="..."> matches page language
  • Language changes within content use lang attribute on containing element

Images & Media

  • All <img> have alt attribute (empty alt="" for decorative)
  • All <img> have explicit width and height (prevents CLS)
  • Complex images (charts, infographics) have long description
  • Videos have captions/subtitles
  • Audio content has transcript
  • No auto-playing media with sound

Color & Contrast

  • Text contrast >= 4.5:1 (normal) / 3:1 (large text >= 24px / 18.5px 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

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

Forms

  • Every input has a programmatic <label> (explicit for/id or wrapping)
  • Placeholder is not used as sole label
  • Required fields indicated visually and with required attribute
  • Error messages linked to inputs via aria-describedby + aria-invalid
  • Form groups use <fieldset> + <legend>
  • Correct type and autocomplete attributes on inputs

ARIA & Dynamic Content

  • Icon-only buttons have aria-label
  • Decorative icons have aria-hidden="true"
  • Dynamic updates use aria-live="polite" or role="status"
  • Error alerts use role="alert"
  • Active navigation links have aria-current="page"
  • Expandable elements use aria-expanded

Motion & Interaction

  • prefers-reduced-motion respected (CSS and JS)
  • No content flashes more than 3 times per second
  • Touch targets meet WCAG 2.2 AA 2.5.8 (minimum 24×24 CSS pixels); use 44×44 as a stricter comfort target where feasible
  • user-scalable=no is NOT set in viewport meta

TYPO3-Specific

  • Backend: alt text fields are filled for all images in File List
  • Backend: content elements have descriptive headers (or header_layout = 100 for hidden)
  • Backend: page properties have proper <title> and description
  • Fluid templates use <f:link.page> / <f:link.typolink> (not <a> with manual hrefs)
  • Content Block / Content Element templates follow accessible patterns below

2. Fluid Template Patterns

2.1 Page Layout with Landmarks

<!-- EXT:site_package/Resources/Private/Layouts/Default.html -->
<!-- Layout files must NOT contain <f:layout> — that tag belongs in Templates to select a layout. -->
<f:render section="Main" />
<!-- 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>
    <!-- Main nav landmark lives in the partial (e.g. MainMenu.html) to avoid nested <nav>. -->
    <f:cObject typoscriptObjectPath="lib.mainNavigation" />
</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

.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

<!-- 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')}"
            additionalAttributes="{decoding: 'async'}"
        />
        <f:if condition="{image.description}">
            <figcaption>{image.description}</figcaption>
        </f:if>
    </figure>
</f:if>

For decorative images (no informational value):

<f:image image="{image}" alt="" />

2.4 Accessible Content Element Wrapper

<!-- 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

<!-- Partial: Resources/Private/Partials/Navigation/MainMenu.html -->
<nav aria-label="{f:translate(key: 'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:nav.main')}">
    <!-- role="list" restores list semantics in Safari/VoiceOver when list-style:none strips native list role -->
    <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)

<div class="accordion" data-accordion>
    <f:for each="{items}" as="item" iteration="iter">
        <div class="accordion__item">
            <!-- Use role="heading" + aria-level so level matches page outline (pass headingLevel 2–6 from CE). -->
            <div role="heading" aria-level="{f:if(condition: headingLevel, then: headingLevel, else: '3')}">
                <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>
            </div>
            <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

<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: '', else: 'hidden')}
        >
            <f:format.html>{item.bodytext}</f:format.html>
        </div>
    </f:for>
</div>

3. PHP: Accessibility Middleware & Helpers

3.1 Language Attribute Middleware (PSR-15)

Ensures <html lang="..."> is always correct based on site language config.

<?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\Site\Entity\SiteLanguage;

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
        );

        $newBody = new \TYPO3\CMS\Core\Http\Stream('php://temp', 'rw');
        $newBody->write($body);

        return $response->withBody($newBody);
    }
}

Register via Configuration/RequestMiddlewares.php (TYPO3 v14; same registration file exists on supported older majors if you maintain multi-version code):

<?php
// Configuration/RequestMiddlewares.php
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 (DataHandler hook)

Warn editors when images lack alt text. Use a processDatamapClass hook on sys_file_metadata saves — not workspace publish events (AfterRecordPublishedEvent lives under Workspaces and fires on publish workflows).

<?php

declare(strict_types=1);

namespace Vendor\SitePackage\Hooks;

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;

final class ImageAltTextValidationHook
{
    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,
            );

            GeneralUtility::makeInstance(FlashMessageService::class)
                ->getMessageQueueByIdentifier()
                ->addMessage($message);
        }
    }
}

Register in ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
    = \Vendor\SitePackage\Hooks\ImageAltTextValidationHook::class;

3.3 Accessible Fluid ViewHelper: SrOnly

Renders screen-reader-only text:

<?php

declare(strict_types=1);

namespace Vendor\SitePackage\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;

final class SrOnlyViewHelper extends AbstractTagBasedViewHelper
{
    protected $tagName = 'span';

    // Fluid 5.x / TYPO3 v14: `registerUniversalTagAttributes()` and `registerTagAttribute()` were
    // removed from AbstractTagBasedViewHelper. Register explicit arguments via `registerArgument()`
    // in `initializeArguments()`; unregistered attributes pass through via handleAdditionalArguments().

    public function render(): string
    {
        $this->tag->addAttribute('class', 'sr-only');
        $this->tag->setContent($this->renderChildren());
        return $this->tag->render();
    }
}
/* 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:

{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>

4. JavaScript: Accessible Widgets

4.1 Accordion

// 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

// 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

// EXT:site_package/Resources/Public/JavaScript/dialog.js
class AccessibleDialog {
    constructor(dialog) {
        this.dialog = dialog;
        this.previousFocus = null;
        this.init();
    }

    init() {
        // showModal() closes on Escape natively; listen to 'close' for focus restore only.
        this.dialog.addEventListener('close', () => {
            this.previousFocus?.focus();
        });

        this.dialog.addEventListener('click', (e) => {
            if (e.target === this.dialog) this.dialog.close();
        });
    }

    open() {
        this.previousFocus = document.activeElement;
        // showModal() provides built-in focus trapping in supporting browsers; skip custom Tab loops unless you must target legacy engines.
        this.dialog.showModal();
    }

    close() {
        this.dialog.close();
    }
}

Use the native <dialog> element in Fluid:

<dialog id="my-dialog" aria-labelledby="dialog-title">
    <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

// 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');
}
@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

// 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');

5. CSS: Focus & Contrast Essentials

/* 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: WCAG 2.2 AA 2.5.8 minimum = 24×24 CSS px (baseline below). */
button, a, input, select, textarea, [role="button"] {
    min-height: 24px;
    min-width: 24px;
}

/* Optional comfort target — intentionally stricter than AA; common for mobile tap areas */
.touch-target-comfort {
    min-height: 44px;
    min-width: 44px;
}

/* Good default; test with user-overridden spacing per 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;
}

6. TYPO3-Specific Configuration

6.1 TCA: Require Alt Text on Images

Make the alt text field required in file metadata:

<?php
// Configuration/TCA/Overrides/sys_file_metadata.php

$GLOBALS['TCA']['sys_file_metadata']['columns']['alternative']['config']['required'] = true;

// On v14 prefer `required` (above). Avoid legacy `eval` = required patterns.

6.2 TCA: Header Layout Options

Provide proper heading levels and a "hidden" option for content elements:

<?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';

Legacy TCA items shape (pre–label/value arrays): numeric keys [0] = label, [1] = value — avoid on v14-only projects; kept here only for reading older extensions:

// Legacy style (do not use for new v14-only TCA)
$GLOBALS['TCA']['tt_content']['columns']['header_layout']['config']['items'] = [
    ['H1', '1'],
    ['H2', '2'],
    ['H3', '3'],
    ['H4', '4'],
    ['H5', '5'],
    ['Hidden', '100'],
];

6.3 Form Framework: Accessible Forms (EXT:form)

# 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'

6.4 Content Security Policy: Allow Inline Focus Styles

If CSP is enabled, ensure focus styles via external CSS (not inline):

<?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\Event\PolicyMutatedEvent;

#[AsEventListener(identifier: 'vendor/site-package/csp-accessibility')]
final class CspAccessibilityListener
{
    public function __invoke(PolicyMutatedEvent $event): void
    {
        if ($event->scope->type->isFrontend()) {
            // Prefer external CSS over extending CSP for inline styles
        }
    }
}

7. Testing & Tools

Automated Testing

ToolUse
axe-core / axe DevToolsBrowser extension for automated WCAG checks
pa11yCLI accessibility testing for CI/CD
LighthouseChrome DevTools audit (Accessibility score)
WAVEBrowser extension for visual feedback

Manual Testing Checklist

  1. Keyboard only: Navigate entire page using Tab, Shift+Tab, Enter, Escape, Arrow keys
  2. Screen reader: Test with VoiceOver (macOS), NVDA/JAWS (Windows)
  3. Zoom: Verify layout at 200% and 400% zoom
  4. Color: Use browser dev tools to simulate color blindness
  5. Reduced motion: Enable prefers-reduced-motion: reduce in OS settings

Pa11y CI Integration

# Run against local DDEV instance
npx pa11y https://mysite.ddev.site --standard WCAG2AA --reporter cli
// .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

ExtensionPurposeTYPO3 v14
typo3/cms-coreBuilt-in ARIA, focus management
brotkrueml/schemaStructured data (WebPage, Article)✓ (verify Packagist)
*(no stable Packagist/TER listing verified)*Third-party accessibility toolbars change often — evaluate extensions individually before recommending
b13/containerAccessible grid/container layouts✓ (verify Packagist)

9. WCAG 2.2 Quick Reference

Success CriterionLevelTYPO3 Solution
1.1.1 Non-text ContentAalt attribute on <f:image>, TCA required field
1.3.1 Info and RelationshipsASemantic HTML in Fluid, <fieldset>/<legend>
1.3.5 Identify Input PurposeAAautocomplete attributes on form fields
1.4.1 Use of ColorAIcons + text alongside color indicators
1.4.3 Contrast (Minimum)AACSS variables with 4.5:1 ratio
1.4.4 Resize TextAArem units, no user-scalable=no
1.4.10 ReflowAAFluid/responsive layouts
1.4.11 Non-text ContrastAA3:1 for UI components
1.4.12 Text SpacingAAline-height: 1.6, flexible containers
2.1.1 KeyboardANative elements, keyboard handlers
2.4.1 Bypass BlocksASkip link
2.4.3 Focus OrderALogical DOM order
2.4.7 Focus VisibleAA:focus-visible styles
2.4.11 Focus Not ObscuredAAscroll-margin-top, no sticky overlaps
2.5.8 Target Size (Minimum)AA24×24 CSS px minimum; larger targets improve usability
3.1.1 Language of PageA<html lang> via middleware
3.1.2 Language of PartsAAlang attribute on multilingual content
3.3.1 Error IdentificationAaria-invalid, aria-describedby
3.3.2 Labels or InstructionsA<label> on every input
4.1.2 Name, Role, ValueAARIA attributes on custom widgets
4.1.3 Status MessagesAAaria-live regions

10. Anti-Patterns (Flag These in Reviews)

Anti-PatternFix
<div onclick="...">Use <button>
outline: none without replacementUse :focus-visible with custom outline
placeholder as sole labelAdd <label> element
tabindex > 0Use tabindex="0" or natural DOM order
user-scalable=no in viewportRemove it
font-size: 12px (absolute)Use rem units
Images without altAdd alt="..." or alt="" for decorative
Color-only error indicationAdd icon + text
<a> without href for actionUse <button>
Missing lang on <html>Set via middleware or Fluid layout
Auto-playing video with soundAdd muted or remove autoplay
Skipped heading levelsFix hierarchy

v14-Only Accessibility Changes

The following accessibility-related changes apply exclusively to TYPO3 v14.

Accessible Combobox Pattern [v14 only]

TYPO3 v14 introduces a new accessible combobox pattern (Forge #106637; do not confuse with unrelated Gerrit change IDs) in the backend. When building custom backend UI widgets, use this pattern instead of custom autocomplete/dropdown implementations.

Native <dialog> Element [v14 only]

Backend modals migrated from Bootstrap Modal to native <dialog> element (Forge #107443). The native dialog provides better accessibility out of the box:

  • Built-in focus trapping
  • Proper aria-modal semantics
  • Escape key handling
  • Inert background
  • No JavaScript framework dependency

Update custom backend JavaScript that listens to Bootstrap modal events such as show.bs.modal / hidden.bs.modal — TYPO3 v14 uses events like typo3-modal-show, typo3-modal-shown, typo3-modal-hide, and typo3-modal-hidden. Also replace Bootstrap modal triggers such as data-bs-toggle="modal" with TYPO3's t3js-modal-trigger class where appropriate.

Fluid 5.x Accessibility Impact [v14 only]

Fluid 5.x is stricter about ViewHelper arguments in some setups. Prefer correct types for numeric and boolean attributes (tabindex, aria-expanded, etc.) and validate templates after upgrades.


Related Skills


Credits & Attribution

Accessibility patterns adapted from:

Source: https://github.com/dirnbauer/webconsulting-skills

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

33.9%
按下载量换算75

Claude

30.79%
按下载量换算68

Cursor

18.48%
按下载量换算41

Gemini CLI

9.84%
按下载量换算22

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills