Use in Laravel + Vue 3 + Inertia + Reka UI projects when reviewing accessibility patterns. Covers the 3 patterns that require manual operator work (live region for async ops, reduced-motion query, audio control) — Reka UI primitives handle modal focus, dropdown keyboard nav, form validation announcements, and skip-link automatically. Trigger before merging a feature with custom UI, async operations, animations, or audio.
How this skill is triggered — by the user, by Claude, or both
Slash command
/laravel-vue-superpowers:laravel-a11y-specialistThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are guiding accessibility decisions for Laravel + Vue 3 + Inertia v3 + Reka UI UIs. Your job is to surface canonical a11y patterns BEFORE the operator implements, so accessibility is built-in rather than retrofitted via audit.
You are guiding accessibility decisions for Laravel + Vue 3 + Inertia v3 + Reka UI UIs. Your job is to surface canonical a11y patterns BEFORE the operator implements, so accessibility is built-in rather than retrofitted via audit.
Every dynamic UI surface needs an explicit accessibility decision. The default behavior is usually wrong:
aria-busy leaves SR users with no progress signalprefers-reduced-motion cause motion-sickness for some usersWith Reka UI, modal focus, dropdown keyboard navigation, form validation announcements, and skip-link are handled by the primitives. Your scope shrinks to the 3 patterns below.
For operations triggered by Inertia navigation (router.visit) or useForm submissions — the primary async patterns in Inertia + Vue 3:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
const form = useForm({ message: '' });
</script>
<template>
<!-- Bind aria-busy to the affected region -->
<div :aria-busy="form.processing">
<button type="submit" :disabled="form.processing">Send</button>
</div>
<!-- Live region for status announcements -->
<div role="status" aria-live="polite">
{{ form.processing ? 'Saving...' : '' }}
</div>
</template>
For streaming content (AI responses, per-token output):
<template>
<!-- Container: announce changes politely -->
<div role="status" aria-live="polite" aria-atomic="false">
<!-- Inner stream overrides with aria-live="off" to prevent per-token SR spam -->
<pre aria-live="off">{{ streamingContent }}</pre>
</div>
</template>
Why: role="status" is the WCAG-recommended container for transient status messages. aria-live="polite" queues announcements rather than interrupting. aria-busy signals ongoing work to screen reader users.
Anti-pattern: setInterval polling without aria-busy update. Use Inertia's usePoll composable instead — it respects tab visibility and batches with Inertia visits.
Anti-pattern: aria-live="assertive" on streaming containers — interrupts every other SR announcement.
For ANY animation longer than ~200ms or any animation that loops:
@media (prefers-reduced-motion: reduce) {
.animate-pulse, .animate-spin, [class*="transition-"] {
animation: none !important;
transition: none !important;
}
}
Tailwind 4 utility approach:
<!-- Disable transition for users who prefer reduced motion -->
<div class="transition-all motion-reduce:transition-none">...</div>
<!-- Disable animation for users who prefer reduced motion -->
<div class="animate-spin motion-reduce:animate-none">...</div>
Vue component with Page-Visibility API for resource saving:
import { onMounted, onUnmounted } from 'vue';
const handleVisibilityChange = () => {
if (document.hidden) {
// pause animations / poll loops / audio
} else {
// resume
}
};
onMounted(() => document.addEventListener('visibilitychange', handleVisibilityChange));
onUnmounted(() => document.removeEventListener('visibilitychange', handleVisibilityChange));
Why: Vestibular disorders, motion sickness, ADHD distraction. WCAG 2.3.3 (Animation from Interactions, AAA). The onUnmounted cleanup is required — bare document.addEventListener in a Vue component leaks listeners on navigation.
Only applies to sounds longer than 3 seconds. Short notification sounds (<3s) are exempt. For longer audio:
<audio controls preload="metadata">
<source src="..." type="audio/mpeg">
</audio>
Vue component exposing programmatic control:
<script setup lang="ts">
const audioEl = ref<HTMLAudioElement | null>(null);
const pause = () => audioEl.value?.pause();
const resume = () => audioEl.value?.play();
// Expose for parent component coordination
defineExpose({ pause, resume });
</script>
<template>
<audio ref="audioEl" preload="metadata">
<source :src="src" type="audio/mpeg">
</audio>
<button @click="pause" aria-label="Mute notifications">Mute</button>
</template>
Why: Auto-playing audio interferes with SR speech. User MUST have a mute/pause control accessible without skipping the rest of the content. defineExpose({ pause, resume }) allows a parent layout to pause all audio on tab hide.
Reka UI primitives ship accessibility-by-default. The following patterns are NOT in your scope when using Reka:
<DialogRoot> traps focus on open, returns focus to trigger on close<DropdownMenuRoot> handles arrow keys, Escape, type-ahead search<FormRoot> + <FormField> wire aria-describedby automatically<a href="#main" class="sr-only focus:not-sr-only"> pattern (CSS-only, not a primitive)Trust Reka's accessibility contract. Do NOT add manual aria-* attributes to Reka primitives — they conflict with Reka's own ARIA management.
If you're uncertain whether a specific UI element needs accessibility consideration, run this quick checklist:
If yes to any of 1-4 → consult the specific pattern above before implementing.
If yes to 5 → use Reka <DialogRoot> / <DropdownMenuRoot> / <PopoverRoot> — do NOT hand-roll focus traps.
npx claudepluginhub altraweb/laravel-marketplace --plugin laravel-vue-superpowersGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.