diff --git a/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx b/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx
new file mode 100644
index 000000000..8fb57293a
--- /dev/null
+++ b/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx
@@ -0,0 +1,95 @@
+/**
+ * Mounted-render accessibility checks for the graph control surfaces:
+ * accessible names on icon-only zoom buttons, disclosure state on the legend,
+ * and a live region on the loading indicator.
+ */
+
+// @vitest-environment happy-dom
+
+import { cleanup, fireEvent, render } from "@testing-library/react"
+import { afterEach, describe, expect, it } from "vitest"
+
+afterEach(cleanup)
+
+import { Legend } from "../components/legend"
+import { LoadingIndicator } from "../components/loading-indicator"
+import { NavigationControls } from "../components/navigation-controls"
+import { DEFAULT_COLORS, DEFAULT_LABELS } from "../constants"
+import type { GraphNode } from "../types"
+
+const node: GraphNode = {
+ id: "doc-1",
+ type: "document",
+ x: 0,
+ y: 0,
+ data: {
+ id: "doc-1",
+ title: "Doc",
+ summary: null,
+ type: "text",
+ createdAt: "2026-01-01",
+ updatedAt: "2026-01-01",
+ memories: [],
+ },
+ size: 40,
+ borderColor: "#fff",
+ isHovered: false,
+ isDragging: false,
+}
+
+const noop = () => {}
+
+describe("NavigationControls accessibility", () => {
+ it("gives the icon-only zoom buttons accessible names", () => {
+ const { getByLabelText } = render(
+ ,
+ )
+
+ expect(getByLabelText("Zoom in")).toBeTruthy()
+ expect(getByLabelText("Zoom out")).toBeTruthy()
+ })
+})
+
+describe("Legend accessibility", () => {
+ it("exposes disclosure state via aria-expanded and toggles it", () => {
+ const { getByRole } = render(
+ ,
+ )
+
+ const toggle = getByRole("button", { name: "Legend" })
+ expect(toggle.getAttribute("aria-expanded")).toBe("false")
+
+ fireEvent.click(toggle)
+ expect(toggle.getAttribute("aria-expanded")).toBe("true")
+ })
+})
+
+describe("LoadingIndicator accessibility", () => {
+ it("announces loading through a status live region", () => {
+ const { getByRole } = render(
+ ,
+ )
+
+ const status = getByRole("status")
+ expect(status.getAttribute("aria-live")).toBe("polite")
+ })
+})
diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx
index e9be3a228..be3267f6a 100644
--- a/packages/memory-graph/src/components/legend.tsx
+++ b/packages/memory-graph/src/components/legend.tsx
@@ -244,6 +244,7 @@ function StatRow({
onClick={expandable ? onToggle : undefined}
style={buttonStyle}
type="button"
+ aria-expanded={expandable ? expanded : undefined}
>
{icon}
@@ -428,6 +429,7 @@ export const Legend = memo(function Legend({
onClick={() => setIsExpanded(!isExpanded)}
style={headerBtnStyle}
type="button"
+ aria-expanded={isExpanded}
>
{isExpanded ? (
diff --git a/packages/memory-graph/src/components/loading-indicator.tsx b/packages/memory-graph/src/components/loading-indicator.tsx
index 8d550eb98..596f9dbdb 100644
--- a/packages/memory-graph/src/components/loading-indicator.tsx
+++ b/packages/memory-graph/src/components/loading-indicator.tsx
@@ -76,7 +76,7 @@ export const LoadingIndicator = memo<
}
return (
-
+
)
},
)
diff --git a/packages/memory-graph/src/components/navigation-controls.tsx b/packages/memory-graph/src/components/navigation-controls.tsx
index 2eec6a81a..d17cdc9c8 100644
--- a/packages/memory-graph/src/components/navigation-controls.tsx
+++ b/packages/memory-graph/src/components/navigation-controls.tsx
@@ -168,6 +168,7 @@ export const NavigationControls = memo
(
onClick={onZoomOut}
style={zoomBtnStyle}
type="button"
+ aria-label="Zoom out"
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "0.8"
}}
@@ -175,12 +176,15 @@ export const NavigationControls = memo(
e.currentTarget.style.opacity = "1"
}}
>
- −
+
+ −
+