- Click the blue button in the bottom-right corner to open the
- accessibility panel. Try adjusting font size, contrast, and other
- settings to see them applied in real-time.
-
-
- This example demonstrates all 15 prebuilt feature controllers working
- together in a single panel.
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
- tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
- veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
- commodo consequat.
-
+
+
+
+
+
+
+ {activeTab === "overview" ? (
+
+
React Accessibility Plugin
+
+ Click the blue button in the bottom-right corner to open the
+ accessibility panel. Try adjusting font size, contrast, spacing,
+ and color filters to see them applied in real-time.
+
+
+ This example demonstrates all 15 prebuilt feature controllers
+ working together with persistent settings, theme adaptation, and
+ dynamic view transitions.
+
+ This tab simulates a dynamic client-side route or page view
+ transition.
+
+
+ Notice how any active preferences (such as enlarged text or
+ increased line height) are immediately applied to this new content
+ as it enters the DOM.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
+ nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi.
+ Nulla quis sem at nibh elementum imperdiet.
+
+ Demonstrates registering site-specific custom features using
+ useAccessibility with provider-owned cleanup and
+ persistence.
+
+
+
+
+
+ Next.js App Router Consumer
+ Next.js 16
+
+
+ A complete Next.js App Router application in
+ examples/next-app/ featuring Server Component layouts,
+ "use client" boundaries, route navigation
+ (Link), dark theme toggling, and strict Content Security
+ Policy headers.
+
+
+ Run locally: npm install && npm run dev in
+ examples/next-app.
+
+
diff --git a/examples/next-app/README.md b/examples/next-app/README.md
new file mode 100644
index 0000000..a60889e
--- /dev/null
+++ b/examples/next-app/README.md
@@ -0,0 +1,65 @@
+# Next.js App Router Consumer Example
+
+This example demonstrates how to integrate `accessibility-plugin-react` into a Next.js App Router application with Server Components, client boundaries, persistent layouts, dark themes, and strict Content Security Policy (CSP).
+
+---
+
+## Key Integration Patterns
+
+### 1. Client Boundary
+
+Keep the widget inside a Client Component:
+
+```tsx
+// app/components/AccessibilitySettings.tsx
+"use client";
+
+import {
+ AccessibilityPlugin,
+ AccessibilityButton,
+ AccessibilityList,
+ FontSizeController,
+ ResetController,
+} from "accessibility-plugin-react";
+
+export function AccessibilitySettings() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+### 2. Persistent Layout
+
+Mount the `` component once inside `app/layout.tsx`. Because `layout.tsx` persists across route transitions, preferences and effect lifecycles are preserved when navigating between pages.
+
+### 3. Strict CSP Guidance
+
+See `next.config.mjs` for a working strict CSP header configuration. When `style-src 'unsafe-inline'` is restricted, import the static stylesheet:
+
+```tsx
+import "accessibility-plugin-react/style.css";
+```
+
+For complete CSP guidance, consult [docs/CSP.md](../../docs/CSP.md).
+
+---
+
+## Running this Example
+
+```sh
+# First, ensure the parent library is built
+npm run build --prefix ../..
+
+# Install and start the Next.js dev server
+npm install
+npm run dev
+```
+
+Visit [http://localhost:3000](http://localhost:3000) to view the application.
diff --git a/examples/next-app/app/about/page.tsx b/examples/next-app/app/about/page.tsx
new file mode 100644
index 0000000..173feab
--- /dev/null
+++ b/examples/next-app/app/about/page.tsx
@@ -0,0 +1,27 @@
+import Link from "next/link";
+
+export default function AboutPage() {
+ return (
+
+
About This Integration
+
+ You are now on the /about route.
+
+
+ Because the AccessibilityPlugin provider is mounted in the
+ root layout.tsx, it maintains persistent effect management
+ and state across route navigations.
+
+
+ When you navigate to this route, any active accessibility preferences
+ (such as enlarged text, custom line height, or contrast palettes) are
+ automatically and safely applied to this newly rendered page content.
+
+ This example demonstrates how to integrate{" "}
+ accessibility-plugin-react into a Next.js App Router
+ application with Server Components, client boundaries, and persistent
+ root layouts.
+
+
Key Architectural Highlights
+
+
+ Client Boundary: The widget is isolated in a{" "}
+ "use client" component (
+ AccessibilitySettings.tsx), keeping the root layout a
+ Server Component.
+
+
+ No Hydration Mismatch:WidgetPortal{" "}
+ defers DOM portal insertion to the client using{" "}
+ useSyncExternalStore, avoiding SSR divergence.
+
+
+ Route Resilience: Click{" "}
+ About Page to navigate. Notice how your
+ accessibility preferences (font size, contrast, spacing) persist
+ across Next.js client-side route transitions without reset or
+ flickering.
+
+
+ Dark Mode Support: Toggle the theme button in the
+ header. The Clean light widget maintains legible contrast against dark
+ host backgrounds without being inverted.
+
>}`,
+ );
+ await mkdir(`${directory}/app/route-test`, { recursive: true });
+ await writeFile(
+ `${directory}/app/route-test/page.jsx`,
+ `import Fixture from '../../Fixture'; import Link from 'next/link'; export default function RoutePage(){return <>
Back to Home
>}`,
);
await writeFile(
`${directory}/pages/_app.jsx`,
diff --git a/tests/browser/widget.spec.ts b/tests/browser/widget.spec.ts
index f5468d1..4163604 100644
--- a/tests/browser/widget.spec.ts
+++ b/tests/browser/widget.spec.ts
@@ -218,6 +218,47 @@ test("Next Pages Router and direct Server Component imports hydrate", async ({
expect(errors).toEqual([]);
});
+test("Next.js App Router client-side route transitions preserve preferences without hydration errors", async ({
+ page,
+}, info) => {
+ test.skip(info.project.name !== "next-app");
+ await page.addInitScript(() => localStorage.clear());
+ const errors: string[] = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+
+ await page.goto("/");
+ await page.getByLabel("Accessibility", { exact: true }).click();
+ await page.getByLabel("Increase font size").click();
+ await page.getByLabel("Close accessibility settings").click();
+
+ // Wait for preference write to storage
+ await page.waitForFunction(() =>
+ localStorage
+ .getItem("react-accessibility-plugin:state")
+ ?.includes("fontSize"),
+ );
+
+ // Navigate to route-test via client-side Next Link
+ await page.locator("#link-route-test").click();
+ await expect(page.locator("#link-home")).toBeVisible();
+
+ // Settings must remain applied on the new route
+ await page.getByLabel("Accessibility", { exact: true }).click();
+ await expect(
+ page
+ .getByRole("group", { name: "Font Size", exact: true })
+ .locator("output"),
+ ).toHaveText("110%");
+ await page.getByLabel("Close accessibility settings").click();
+
+ // Navigate back to home
+ await page.locator("#link-home").click();
+ await expect(page.locator("#link-route-test")).toBeVisible();
+
+ expect(errors).toEqual([]);
+ await page.evaluate(() => localStorage.clear());
+});
+
test("trigger drag remembers placement and preferences without accidental activation", async ({
page,
browserName,
diff --git a/tests/consumerSsr.test.tsx b/tests/consumerSsr.test.tsx
new file mode 100644
index 0000000..839229e
--- /dev/null
+++ b/tests/consumerSsr.test.tsx
@@ -0,0 +1,161 @@
+import { describe, it, expect } from "vitest";
+import React from "react";
+import { renderToString } from "react-dom/server";
+import {
+ AccessibilityPlugin,
+ AccessibilityButton,
+ AccessibilityList,
+ FontSizeController,
+ LineHeightController,
+ LetterSpacingController,
+ ContrastController,
+ SaturationController,
+ InvertColorsController,
+ GrayscaleController,
+ LinkHighlightController,
+ TextAlignController,
+ HideImagesController,
+ HideVideosController,
+ BigCursorController,
+ ReadingMaskController,
+ ReadingGuideController,
+ ResetController,
+ FeatureCard,
+ ToggleControl,
+ ChoiceControl,
+ StepperControl,
+ ProfilePreviewCard,
+ PROFILE_DEFINITIONS,
+ SearchCount,
+ EmptyState,
+} from "../src";
+
+describe("Server-Side Rendering (SSR) & Consumer Boundaries", () => {
+ it("renders AccessibilityPlugin and all 15 controllers to string without errors or browser-global access", () => {
+ // During SSR in Next.js Server Components, window/localStorage are either undefined or should never be accessed.
+ const html = renderToString(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Server Content
+
Initial server rendered text
+
+ ,
+ );
+
+ // Host children must render
+ expect(html).toContain("Server Content");
+ expect(html).toContain("Initial server rendered text");
+
+ // Client portal must return null on the server to prevent hydration mismatch
+ expect(html).not.toContain("arap-root");
+ expect(html).not.toContain("arap-panel");
+ });
+
+ it("renders control primitives and profile cards to string without errors", () => {
+ const html = renderToString(
+