Per-user accessibility preferences (reduced motion, high contrast, text size, underline links, enhanced focus) persisted server-side + applied flash-free, plus screen-reader primitives (skip link, live-region announcer). Activates when wiring a11y settings, the panel, or SR support.
php artisan boost:add-skill happones/kinetix
Save this content to: .claude/skills/kinetix-accessibility/SKILL.md
# Kinetix Accessibility Development
## When to Apply
Activate this skill when:
- Mounting `<KinetixAccessibilityPanel>` or wiring per-user a11y preferences.
- Adding the compact `<KinetixAccessibilityMenu>` (popover) to the header, login
page or setup wizard (guest-safe — server persist is best-effort).
- Adding the `<KinetixModeToggle>` dark-mode button (Light/Dark/System), backed by
`useKinetixAppearance` which shares the starter kit's `appearance` storage.
- Installing the `KinetixAccessibility` Vue plugin.
- Adding a `<KinetixSkipLink>` or announcing async updates to screen readers.
## Documentation
For full details, reference `docs/accessibility.md` (published at https://happones.github.io/kinetix/accessibility).
## Configuration
```bash
php artisan vendor:publish --tag=kinetix-accessibility-migrations && php artisan migrate
```
```php
'accessibility' => [
'enabled' => env('KINETIX_ACCESSIBILITY_ENABLED', false),
'defaults' => ['reducedMotion' => false, 'highContrast' => false, 'textSize' => 'normal', 'underlineLinks' => false, 'enhancedFocus' => false],
],
```
## How it works
- **Per-user, self-service**: `GET/POST {prefix}/accessibility` (`AccessibilityController`),
one `kinetix_accessibility` row per user (`AccessibilityManager::for/update`,
merged over config defaults; `AccessibilityData` DTO normalizes values + textSize enum).
- **Shared + applied flash-free**: the prefs ship on every Inertia response as
`kinetix_accessibility`. Install the **`KinetixAccessibility` plugin**
(`app.use(...)`) — it injects the a11y CSS and applies classes to `<html>`
before mount, reading the initial `data-page` prop + a localStorage mirror.
- **Classes on `<html>`**: `kx-reduce-motion`, `kx-high-contrast`,
`kx-text-large`/`kx-text-x-large`, `kx-underline-links`, `kx-enhanced-focus`
(CSS is injected by the plugin — theme-agnostic, `currentColor` outlines).
## Frontend
```vue
<KinetixAccessibilityPanel /> <!-- text-size segmented control + 4 toggles -->
```
- `useKinetixAccessibility()` → `{ prefs, set(key, value) }` (optimistic apply +
persist + localStorage). `applyKinetixAccessibility(prefs)` is the standalone
class-applier (used by the plugin).
- `<KinetixSkipLink target="#main" />` — visually hidden until focused.
- `useKinetixAnnounce()` → `announce(message, assertive?)` via a shared ARIA
live region for SR-only announcements (toasts, counts, removed rows).
- `useKinetixReducedMotion()` → reactive `boolean` for motion CSS can't stop
(timers, `<Transition>` durations): OS `prefers-reduced-motion` OR the
`kx-reduce-motion` class OR `kinetix.motion = 'reduced'` (config, shared as
`kinetix_config.motion`). One shared media listener + class observer,
released with the last consumer. `useKinetixTransition(preset)` builds on it
(`fade` · `slide-down` · `slide-up` · `scale` · `collapse` · `none`).
- `focusableNear(el)` (from `useKinetixFocusTrap`) → where focus goes when a
focused element leaves the page (next tab stop, else previous).
i18n `a11y_*` / `skip_to_content` (en/es/fr/pt). Tests: `AccessibilityTest`,
`useKinetixAccessibility.spec.ts`.
## UUID / ULID Host Models
This feature's migration builds `user_id` with
`Happones\Kinetix\Support\HostKeys`, which types each column after YOUR model
at migrate time from its primary key (`HasUlids` -> ulid, `HasUuids` -> uuid, string `$keyType` ->
string, else bigint). Pin `kinetix.key_types.user|team` when detection cannot
see the setup; morph ids follow `kinetix.key_types.morph` (default bigint) —
set it when the referenced models use UUIDs/ULIDs. Apps migrated on an older
Kinetix have bigint columns on disk and need their own ALTER migration. Full
recipe: the `kinetix-boost` skill, section "UUID / ULID Host Models".
Activate this skill when:
<KinetixAccessibilityPanel> or wiring per-user a11y preferences.<KinetixAccessibilityMenu> (popover) to the header, login
page or setup wizard (guest-safe — server persist is best-effort).<KinetixModeToggle> dark-mode button (Light/Dark/System), backed by
useKinetixAppearance which shares the starter kit's appearance storage.KinetixAccessibility Vue plugin.<KinetixSkipLink> or announcing async updates to screen readers.For full details, reference docs/accessibility.md (published at https://happones.github.io/kinetix/accessibility).
php artisan vendor:publish --tag=kinetix-accessibility-migrations && php artisan migrate
'accessibility' => [
'enabled' => env('KINETIX_ACCESSIBILITY_ENABLED', false),
'defaults' => ['reducedMotion' => false, 'highContrast' => false, 'textSize' => 'normal', 'underlineLinks' => false, 'enhancedFocus' => false],
],
GET/POST {prefix}/accessibility (AccessibilityController),
one kinetix_accessibility row per user (AccessibilityManager::for/update,
merged over config defaults; AccessibilityData DTO normalizes values + textSize enum).kinetix_accessibility. Install the KinetixAccessibility plugin
(app.use(...)) — it injects the a11y CSS and applies classes to <html>
before mount, reading the initial data-page prop + a localStorage mirror.<html>: kx-reduce-motion, kx-high-contrast,
kx-text-large/kx-text-x-large, kx-underline-links, kx-enhanced-focus
(CSS is injected by the plugin — theme-agnostic, currentColor outlines).<KinetixAccessibilityPanel /> <!-- text-size segmented control + 4 toggles -->
useKinetixAccessibility() → { prefs, set(key, value) } (optimistic apply +
persist + localStorage). applyKinetixAccessibility(prefs) is the standalone
class-applier (used by the plugin).<KinetixSkipLink target="#main" /> — visually hidden until focused.useKinetixAnnounce() → announce(message, assertive?) via a shared ARIA
live region for SR-only announcements (toasts, counts, removed rows).useKinetixReducedMotion() → reactive boolean for motion CSS can't stop
(timers, <Transition> durations): OS prefers-reduced-motion OR the
kx-reduce-motion class OR kinetix.motion = 'reduced' (config, shared as
kinetix_config.motion). One shared media listener + class observer,
released with the last consumer. useKinetixTransition(preset) builds on it
(fade · slide-down · slide-up · scale · collapse · none).focusableNear(el) (from useKinetixFocusTrap) → where focus goes when a
focused element leaves the page (next tab stop, else previous).i18n a11y_* / skip_to_content (en/es/fr/pt). Tests: AccessibilityTest,
useKinetixAccessibility.spec.ts.
This feature's migration builds user_id with
Happones\Kinetix\Support\HostKeys, which types each column after YOUR model
at migrate time from its primary key (HasUlids -> ulid, HasUuids -> uuid, string $keyType ->
string, else bigint). Pin kinetix.key_types.user|team when detection cannot
see the setup; morph ids follow kinetix.key_types.morph (default bigint) —
set it when the referenced models use UUIDs/ULIDs. Apps migrated on an older
Kinetix have bigint columns on disk and need their own ALTER migration. Full
recipe: the kinetix-boost skill, section "UUID / ULID Host Models".
How can I help you explore Laravel packages today?