From craft-workspace-webconsulting-skills
Audits and implements TYPO3 accessibility patterns for WCAG 2.2 AA, including Fluid templates, PHP helpers, JavaScript widgets, forms, focus states, ARIA, and go-live checks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/craft-workspace-webconsulting-skills:typo3-accessibilityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Source: https://github.com/dirnbauer/webconsulting-skills
references/03-php-accessibility-middleware-and-helpers.mdreferences/04-javascript-accessible-widgets.mdreferences/05-css-focus-and-contrast-essentials.mdreferences/06-typo3-specific-configuration.mdreferences/07-testing-and-tools.mdreferences/08-accessibility-extensions.mdreferences/09-wcag-2-2-quick-reference.mdreferences/10-anti-patterns-flag-these-in-reviews.mdreferences/credits-and-attribution.mdreferences/full-guide.mdreferences/related-skills.mdreferences/v14-only-accessibility-changes.mdCompatibility: 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.
Run through this checklist before every deployment. Mark items as you fix them.
<h1>h1 > h2 > h3, no skips)<main>, <nav>, <header>, <footer>, <aside><html lang="..."> matches page languagelang attribute on containing element<img> have alt attribute (empty alt="" for decorative)<img> have explicit width and height (prevents CLS):focus-visible)<label> (explicit for/id or wrapping)required attributearia-describedby + aria-invalid<fieldset> + <legend>type and autocomplete attributes on inputsaria-labelaria-hidden="true"aria-live="polite" or role="status"role="alert"aria-current="page"aria-expandedprefers-reduced-motion respected (CSS and JS)user-scalable=no is NOT set in viewport metaheader_layout = 100 for hidden)<title> and description<f:link.page> / <f:link.typolink> (not <a> with manual hrefs)<!-- 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>
.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;
}
<!-- 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="" />
<!-- 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>
<!-- 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>
<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>
<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>
Read the full guide when the task needs detailed examples, long templates, troubleshooting matrices, appendices, or sections not included above. Keep this file unloaded for narrow tasks so the skill follows progressive disclosure.
npx claudepluginhub dirnbauer/webconsulting-skillsProvides battle-tested TYPO3 Fluid template patterns for v12+ site packages, covering template hierarchy, CMS-first content architecture, responsive images, and WCAG 2.1 AA accessibility.
Simplifies and refines TYPO3 extension code (PHP, Fluid, TCA, YAML) for v14 best practices, replacing deprecated patterns with core APIs. Run after implementing a feature or before merging a PR.
Web accessibility discipline: semantic HTML first, ARIA only when needed, keyboard access always. Invoke whenever task involves any interaction with accessible web content -- writing, reviewing, refactoring, or debugging HTML/CSS/JS for WCAG compliance, ARIA usage, keyboard navigation, focus management, screen reader support, or accessible component patterns.