Search by

aotr / open-a11y-laravel

aotr

The open-a11y accessibility widget for Laravel Blade, Livewire, and Inertia React/Vue/Svelte.

Package info

github.com/aotr/open-a11y-laravel

pkg:composer/aotr/open-a11y-laravel

Statistics

Installs: 24

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-10-27 06:23 UTC

This package is auto-updated.

Last update: 2026-10-27 06:23:58 UTC


README

Latest Version on Packagist Tests License: MIT PHP 8.2+

An accessibility widget with a real engine behind it — made native to Laravel.

open-a11y ships profiles, individual tools, keyboard shortcuts, persisted preferences, and a shadow-DOM interface. This package gives that runtime a Blade component, a Livewire component, publishable config, and a standalone browser bundle. It also includes complete Inertia React, Vue, and Svelte example pages. No Node, Vite, or front-end build is required for the Blade/Livewire package itself.

open-a11y Laravel surfaces

<x-open-a11y::widget :config="['position' => 'bottom-right', 'brand' => ['primary' => '#0f6b4f']]" />

Requirements

  • PHP 8.2+
  • Laravel 12 or 13
  • Livewire 3 or 4 (for the Livewire component)
  • Optional: Inertia 3 with React 19 or Vue 3 (for the Inertia examples)
  • Node.js + pnpm only for rebuilding the Inertia demo assets
  • A browser with Web Components and Shadow DOM: Chrome 90+, Firefox 90+, Safari 15+, Edge 90+

The Blade component is independent of Livewire. The package requires Livewire because it ships both integrations.

Installation

composer require aotr/open-a11y-laravel

Laravel package discovery registers Aotr\OpenA11y\OpenA11yServiceProvider automatically.

Publish the browser bundle:

php artisan vendor:publish --tag=open-a11y-assets

This copies the runtime to public/vendor/open-a11y/. The Blade and Livewire components inject the <script> tags once per page, so you do not need to add them to your layout.

Optionally publish the config file:

php artisan vendor:publish --tag=open-a11y-config

Set OPEN_A11Y_ASSET_URL (or open-a11y.assets.url) when serving the bundle from a CDN. The published public/vendor/open-a11y directory must be web accessible when using the default configuration.

About the JavaScript core

The framework-agnostic runtime is available on npm as @aotr/open-a11y:

npm install @aotr/open-a11y

This package ships the standalone IIFE browser bundle in resources/dist/open-a11y.js, so Laravel applications can use the widget without a JavaScript toolchain. If your app already bundles the npm core, point open-a11y.assets.url at your built asset (or publish and edit the package views).

Blade

<x-open-a11y::widget
    :config="[
        'position' => 'bottom-right',
        'side' => 'right',
        'offset' => [24, 24],
        'blur' => true,
        'buttonSize' => 56,
        'showButton' => true,
        'lang' => 'en',
        'brand' => [
            'name' => 'Acme',
            'url' => 'https://acme.example',
            'footerText' => 'Accessibility by Acme',
            'primary' => '#0f6b4f',
            'primaryDark' => '#0a4a37',
            'accent' => '#d4a13a',
        ],
        'labels' => [
            'widget.menuTitle' => 'Accessibility options',
        ],
    ]"
/>

The component merges your array over the package defaults in config/open-a11y.php, renders a hidden mount element with the JSON config, and includes the two browser scripts once.

Pass a stable widget-key when you want to target the instance from your own JavaScript:

<x-open-a11y::widget :config="$config" widget-key="site-widget" />

Livewire

<livewire:open-a11y::widget
    position="bottom-right"
    side="right"
    :offset-x="24"
    :offset-y="24"
    :blur="true"
    :button-size="56"
    :show-button="true"
    primary="#0f6b4f"
    primary-dark="#0a4a37"
    accent="#d4a13a"
    footer-name="Acme"
    footer-url="https://acme.example"
    footer-text="Accessibility by Acme"
    menu-title="Accessibility options"
    language="en"
/>

Every property is reactive. When a property changes, the component dispatches a browser event with the new config:

window.addEventListener("open-a11y:update", (event) => {
  // event.detail = { key: 'open-a11y-…', config: { … } }
});

The package's auto-init script listens for that event and remounts the widget. The underlying engine is shared across mounts, so the visitor's selected tools, contrast modes, and language stay in localStorage — a color change does not reset their preferences.

Bind properties from a parent Livewire component or update them on the component itself; both paths flow through the same event.

Inertia + React

The repository includes a full Inertia React page at /inertia-react.

Inertia React demo

The package itself is framework-neutral; use the official React adapter in your Inertia entry:

npm install @aotr/open-a11y @inertiajs/react react react-dom
// resources/js/app.tsx
import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";
import { AccessibilityEngine, localStorageAdapter } from "@aotr/open-a11y";
import { AccessibilityProvider } from "@aotr/open-a11y/react";

const engine = new AccessibilityEngine({ storage: localStorageAdapter });

createInertiaApp({
  resolve: (name) => import(`./pages/${name}.tsx`),
  setup({ el, App, props }) {
    createRoot(el).render(
      <AccessibilityProvider engine={engine} autoInit>
        <App {...props} />
      </AccessibilityProvider>,
    );
  },
});
// resources/js/pages/Welcome.tsx
import { AccessibilityWidget } from "@aotr/open-a11y/react";

export default function Welcome({ config }) {
  return (
    <>
      <h1>Your Inertia page</h1>
      <AccessibilityWidget config={config} />
    </>
  );
}

Inertia + Vue

Inertia Vue demo

npm install @aotr/open-a11y @inertiajs/vue3 vue
// resources/js/app.ts
import { createApp, h } from "vue";
import { createInertiaApp } from "@inertiajs/vue3";
import { AccessibilityEngine, localStorageAdapter } from "@aotr/open-a11y";
import { createAccessibilityPlugin } from "@aotr/open-a11y/vue";

const engine = new AccessibilityEngine({ storage: localStorageAdapter });

createInertiaApp({
  resolve: (name) => import(`./pages/${name}.vue`),
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .use(createAccessibilityPlugin(engine))
      .mount(el);
  },
});
<script setup lang="ts">
import { A11yWidget } from "@aotr/open-a11y/vue";

defineProps<{ config: Record<string, unknown> }>();
</script>

<template>
  <h1>Your Inertia page</h1>
  <A11yWidget :options="{ config }" />
</template>

Inertia + Svelte

Inertia Svelte demo

npm install @aotr/open-a11y @inertiajs/svelte svelte
// resources/js/app.ts
import { createInertiaApp } from "@inertiajs/svelte";
import { mount } from "svelte";
import { AccessibilityEngine, localStorageAdapter } from "@aotr/open-a11y";

const engine = new AccessibilityEngine({ storage: localStorageAdapter });

void engine.init().then(() =>
  createInertiaApp({
    resolve: (name) => import(`./pages/${name}.svelte`),
    setup({ el, App, props }) {
      mount(App, { target: el, props });
    },
  }),
);
<script lang="ts">
    import { accessibilityWidget } from "@aotr/open-a11y/svelte";
    import { engine } from "./accessibility-engine";

    let { config } = $props();
    const widgetOptions = { engine, config };
</script>

<h1>Your Inertia page</h1>
<div use:accessibilityWidget={widgetOptions}></div>

All three Inertia examples use the same server-resolved config as Blade and Livewire. The demo pages are in workbench/resources/js/pages.

Customization

Blade passes the runtime config keys directly. Livewire exposes the most useful knobs as typed public properties.

Livewire property Blade config key Type Default
position position bottom-right, bottom-left, top-right, top-left bottom-right
side side right, left right
offsetX, offsetY offset [int, int] [20, 20]
blur blur bool false
buttonSize buttonSize int (px) 56
showButton showButton bool true
primary brand.primary CSS color #0f6b4f
primaryDark brand.primaryDark CSS color #0a4a37
accent brand.accent CSS color #d4a13a
footerName brand.name string ''
footerUrl brand.url string ''
footerText brand.footerText string ''
menuTitle labels.widget.menuTitle string Accessibility options
language lang en, hi en
widgetKey widget-key attribute string generated

An empty footerText hides the footer. When it is omitted and brand.name is set, the runtime falls back to Accessibility by {name}.

Package-level defaults live in config/open-a11y.php:

return [
    'position' => 'bottom-right',
    'side' => 'right',
    'offset' => [20, 20],
    'blur' => false,
    'button_size' => 56,
    'show_button' => true,
    'language' => 'en',
    'menu_title' => 'Accessibility options',
    'brand' => [
        'name' => null,
        'url' => null,
        'footer_text' => null,
        'primary' => '#0f6b4f',
        'primary_dark' => '#0a4a37',
        'accent' => '#d4a13a',
    ],
    'assets' => [
        'path' => 'vendor/open-a11y',
        'url' => null, // e.g. https://cdn.example.com/a11y
    ],
];

Need a custom UI string that is not exposed as a Livewire property? Use the Blade component and pass it through labels:

<x-open-a11y::widget :config="[
    'labels' => [
        'widget.menuTitle' => 'Reading options',
        'widget.reset' => 'Clear all settings',
    ],
]" />

Demo

The repository includes a Testbench workbench with four demos:

  • /blade — Blade component with a plain-JavaScript customizer
  • /livewire — Livewire component with server-reactive controls
  • /inertia-react — Inertia page powered by React 19
  • /inertia-vue — Inertia page powered by Vue 3
  • /inertia-svelte — Inertia page powered by Svelte 5
composer install
pnpm install
composer demo

composer demo builds the Inertia entries and starts the workbench on http://127.0.0.1:4179. The Blade and Livewire assets are served from resources/dist; the Inertia entries are built with Vite+ and served from the Testbench public directory.

Open any of the four routes from the workbench navigation. The Inertia components live in workbench/resources/js/pages.

Testing

composer test

The suite uses Orchestra Testbench and PHPUnit. It covers:

  • Blade component rendering, JSON config serialization, and attribute merging
  • Livewire initial props, browser event dispatch, and language updates
  • Service provider view registration and publish tags
  • All five demo routes and the asset routes
  • Inertia React, Vue, and Svelte page components and server props
  • Config defaults and override precedence

Publishing

Tag Copies
open-a11y-config config/open-a11y.php → config/open-a11y.php
open-a11y-views resources/views → resources/views/vendor/open-a11y
open-a11y-assets resources/dist → public/vendor/open-a11y
open-a11y all of the above
php artisan vendor:publish --tag=open-a11y

Use --force to overwrite previously published files.

How it fits together

<x-open-a11y::widget>            <livewire:open-a11y::widget>
        │                                  │
        └──────────────┬───────────────────┘
                       ▼
          hidden [data-open-a11y] element
          with JSON config attribute
                       │
                       ▼
          resources/dist/open-a11y-laravel.js
          reads config · owns shared engine
          persists to localStorage · remounts
                       │
                       ▼
          resources/dist/open-a11y.js (IIFE)
          web components · shadow DOM · tools

The widget's shadow DOM is intentionally left alone by the package and the demo. Brand colors and copy are passed through the runtime config; the host page keeps control of its own typography, spacing, and layout.

The upstream runtime maintains one active floating widget per page. Render one open-a11y component per page and use widget-key to target it from custom JavaScript.

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for the local setup, the test command, and the review checklist.

License

open-a11y Laravel is open-source software licensed under the MIT license.