[A11y] Create a11y page with placeholders. (#9889)
The screen is gated by the FeatureFlags.accessibility flag.
internal link: go/devtool-a11y-userjourney
### General checklist
### Issues checklist
### Tests checklist
### AI-tooling checklist
* [ ] I read the [AI contributions guidelines] and agree to follow them.
* [ ] I reviewed all AI-generated code before opening this PR.
* [ ] I understand and am able to discuss the code in this PR.
* [ ] I have verifed the accuracy of any AI-generated text included in the PR description.
* [ ] I commit to verifying the accuracy of any AI-generated code or text that I upload in response to review comments.
### Feature-change checklist
* [ ] I added the `release-notes-not-required` label or left a comment requesting the label be added.
* [ ] I added an entry to `packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md`.
* [ ] I included before/after screenshots and/or a GIF demo of the new UI to my PR description.
* [ ] I ran the DevTools app locally to manually verify my changes.
![build.yaml badge]
If you need help, consider asking for help on [Discord].
[`contributions-welcome`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Acontributions-welcome
[`good-first-issue`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Agood-first-issue
[build.yaml badge]: https://github.com/flutter/devtools/actions/workflows/build.yaml/badge.svgdiff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart
index 5125d71..fb2637e 100644
--- a/packages/devtools_app/lib/devtools_app.dart
+++ b/packages/devtools_app/lib/devtools_app.dart
@@ -10,6 +10,8 @@
export 'src/framework/release_notes.dart';
export 'src/framework/scaffold/app_bar.dart';
export 'src/framework/scaffold/status_line.dart';
+export 'src/screens/accessibility/accessibility_controller.dart';
+export 'src/screens/accessibility/accessibility_screen.dart';
export 'src/screens/app_size/app_size_controller.dart';
export 'src/screens/app_size/app_size_screen.dart';
export 'src/screens/debugger/breakpoint_manager.dart';
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index 272f1fc..e51ec18 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -23,6 +23,8 @@
import 'framework/observer/disconnect_observer.dart';
import 'framework/release_notes.dart';
import 'framework/scaffold/scaffold.dart';
+import 'screens/accessibility/accessibility_controller.dart';
+import 'screens/accessibility/accessibility_screen.dart';
import 'screens/app_size/app_size_controller.dart';
import 'screens/app_size/app_size_screen.dart';
import 'screens/debugger/debugger_controller.dart';
@@ -713,6 +715,11 @@
LoggingScreen(),
createController: (_) => LoggingController(),
),
+ if (FeatureFlags.accessibility.isEnabled)
+ DevToolsScreen<AccessibilityController>(
+ AccessibilityScreen(),
+ createController: (_) => AccessibilityController(),
+ ),
DevToolsScreen<DevToolsScreenController>(ProviderScreen()),
DevToolsScreen<AppSizeController>(
AppSizeScreen(),
diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
new file mode 100644
index 0000000..69f6dd1
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
@@ -0,0 +1,67 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app_shared/utils.dart';
+import 'package:flutter/foundation.dart';
+
+import '../../shared/framework/screen.dart';
+import '../../shared/framework/screen_controllers.dart';
+
+/// Controller for the Accessibility screen.
+class AccessibilityController extends DevToolsScreenController
+ with AutoDisposeControllerMixin {
+ AccessibilityController() {
+ _initListeners();
+ }
+
+ void _initListeners() {
+ addAutoDisposeListener(brightness, _onBrightnessChanged);
+ addAutoDisposeListener(textScale, _onTextScaleChanged);
+ addAutoDisposeListener(boldText, _onBoldTextChanged);
+ addAutoDisposeListener(screenReader, _onScreenReaderChanged);
+ addAutoDisposeListener(highContrast, _onHighContrastChanged);
+ }
+
+ void _onBrightnessChanged() {
+ // TODO(hannah-hyj): Implement VM service extension call for brightness override.
+ // e.g. using 'ext.flutter.brightnessOverride'.
+ }
+
+ void _onTextScaleChanged() {
+ // TODO(hannah-hyj): Implement VM service extension call for text scale override.
+ }
+
+ void _onBoldTextChanged() {
+ // TODO(hannah-hyj): Implement VM service extension call for bold text override.
+ }
+
+ void _onScreenReaderChanged() {
+ // TODO(hannah-hyj): Implement VM service extension call for screen reader / semantics debugger.
+ // e.g. using 'ext.flutter.showSemanticsDebugger'.
+ }
+
+ void _onHighContrastChanged() {
+ // TODO(hannah-hyj): Implement VM service extension call for high contrast override.
+ }
+
+ @override
+ final screenId = ScreenMetaData.accessibility.id;
+
+ // --- Accessibility Overrides State ---
+ final brightness = ValueNotifier<String>('System');
+ final textScale = ValueNotifier<double>(1.0);
+ final boldText = ValueNotifier<bool>(false);
+ final screenReader = ValueNotifier<bool>(false);
+ final highContrast = ValueNotifier<bool>(false);
+
+ @override
+ void dispose() {
+ brightness.dispose();
+ textScale.dispose();
+ boldText.dispose();
+ screenReader.dispose();
+ highContrast.dispose();
+ super.dispose();
+ }
+}
diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
new file mode 100644
index 0000000..6c3efd2
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
@@ -0,0 +1,94 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:flutter/material.dart';
+
+import '../../shared/framework/screen.dart';
+import '../../shared/globals.dart';
+import '../../shared/ui/common_widgets.dart';
+import 'accessibility_controller.dart';
+
+/// A screen that displays accessibility information.
+class AccessibilityScreen extends Screen {
+ AccessibilityScreen() : super.fromMetaData(ScreenMetaData.accessibility);
+
+ static final id = ScreenMetaData.accessibility.id;
+
+ @override
+ Widget buildScreenBody(BuildContext context) =>
+ const AccessibilityScreenBody();
+}
+
+class AccessibilityScreenBody extends StatefulWidget {
+ const AccessibilityScreenBody({super.key});
+
+ @override
+ State<AccessibilityScreenBody> createState() =>
+ _AccessibilityScreenBodyState();
+}
+
+class _AccessibilityScreenBodyState extends State<AccessibilityScreenBody>
+ with AutoDisposeMixin {
+ late AccessibilityController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = screenControllers.lookup<AccessibilityController>();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final splitAxis = _splitAxisFor(context);
+ return SplitPane(
+ axis: splitAxis,
+ initialFractions: const [0.6, 0.4],
+ children: const [
+ AccessibilitySemanticsTreePane(),
+ AccessibilityOverridesPane(),
+ ],
+ );
+ }
+
+ Axis _splitAxisFor(BuildContext context) {
+ final screenSize = MediaQuery.of(context).size;
+ return screenSize.width > 1000 ? Axis.horizontal : Axis.vertical;
+ }
+}
+
+/// A pane that displays the semantics tree of the connected app.
+class AccessibilitySemanticsTreePane extends StatelessWidget {
+ const AccessibilitySemanticsTreePane({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const DevToolsAreaPane(
+ header: AreaPaneHeader(title: Text('Semantics Tree')),
+ child: CenteredMessage(
+ message:
+ 'Accessibility semantics tree placeholder.\n'
+ '// TODO(hannah-hyj): Implement semantics tree view and details explorer.',
+ ),
+ );
+ }
+}
+
+/// A pane that displays the accessibility overrides controls.
+class AccessibilityOverridesPane extends StatelessWidget {
+ const AccessibilityOverridesPane({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const DevToolsAreaPane(
+ header: AreaPaneHeader(title: Text('Accessibility Overrides')),
+ child: CenteredMessage(
+ message:
+ 'Accessibility overrides placeholder.\n'
+ '// TODO(hannah-hyj): Implement setting overrides (brightness, text scale, bold text, screen reader, high contrast).',
+ ),
+ );
+ }
+}
diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart
index 0788278..937c751 100644
--- a/packages/devtools_app/lib/src/shared/feature_flags.dart
+++ b/packages/devtools_app/lib/src/shared/feature_flags.dart
@@ -69,6 +69,12 @@
enabled: enableExperiments,
);
+ /// Flag to enable the Accessibility page.
+ static final accessibility = BooleanFeatureFlag(
+ name: 'accessibility',
+ enabled: enableExperiments,
+ );
+
/// A set of all the boolean feature flags for debugging purposes.
///
/// When adding a new boolean flag, you are responsible for adding it to this
@@ -77,6 +83,7 @@
memorySaveLoad,
devToolsExtensions,
dapDebugging,
+ accessibility,
};
/// A set of all the Flutter channel feature flags for debugging purposes.
diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart
index a2544f2..2bfe341 100644
--- a/packages/devtools_app/lib/src/shared/framework/screen.dart
+++ b/packages/devtools_app/lib/src/shared/framework/screen.dart
@@ -94,6 +94,12 @@
supportsWebServerDevice: true,
tutorialVideoTimestamp: '?t=558',
),
+ accessibility(
+ 'accessibility',
+ title: 'Accessibility',
+ icon: Icons.accessibility,
+ requiresFlutter: true,
+ ),
provider(
'provider',
title: 'Provider',
diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
new file mode 100644
index 0000000..595cf4a
--- /dev/null
+++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
@@ -0,0 +1,80 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+@TestOn('vm')
+library;
+
+import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:devtools_test/devtools_test.dart';
+import 'package:devtools_test/helpers.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+void main() {
+ late AccessibilityScreen screen;
+ late AccessibilityController controller;
+ const windowSize = Size(1000.0, 1000.0);
+
+ group('Accessibility Screen', () {
+ Future<void> pumpAccessibilityScreen(WidgetTester tester) async {
+ await tester.pumpWidget(
+ wrapWithControllers(
+ const AccessibilityScreenBody(),
+ accessibility: controller,
+ ),
+ );
+ }
+
+ setUp(() {
+ final fakeServiceConnection = FakeServiceConnectionManager();
+ when(
+ fakeServiceConnection.serviceManager.connectedApp!.isFlutterWebAppNow,
+ ).thenReturn(false);
+ when(
+ fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow,
+ ).thenReturn(false);
+ when(
+ fakeServiceConnection.errorBadgeManager.errorCountNotifier(
+ 'accessibility',
+ ),
+ ).thenReturn(ValueNotifier<int>(0));
+
+ setGlobal(NotificationService, NotificationService());
+ setGlobal(
+ DevToolsEnvironmentParameters,
+ ExternalDevToolsEnvironmentParameters(),
+ );
+ setGlobal(PreferencesController, PreferencesController());
+ setGlobal(ServiceConnectionManager, fakeServiceConnection);
+ setGlobal(IdeTheme, IdeTheme());
+
+ controller = AccessibilityController();
+ screen = AccessibilityScreen();
+ });
+
+ testWidgets('builds its tab', (WidgetTester tester) async {
+ await tester.pumpWidget(wrap(Builder(builder: screen.buildTab)));
+ expect(find.text('Accessibility'), findsOneWidget);
+ });
+
+ testWidgetsWithWindowSize('builds split view with panes', windowSize, (
+ WidgetTester tester,
+ ) async {
+ await pumpAccessibilityScreen(tester);
+ await tester.pumpAndSettle();
+
+ expect(find.byType(AccessibilityScreenBody), findsOneWidget);
+ expect(find.byType(SplitPane), findsAtLeastNWidgets(1));
+
+ // Overrides pane should be visible
+ expect(find.byType(AccessibilityOverridesPane), findsOneWidget);
+
+ // Semantics Tree pane should be visible
+ expect(find.byType(AccessibilitySemanticsTreePane), findsOneWidget);
+ });
+ });
+}
diff --git a/packages/devtools_test/lib/src/helpers/wrappers.dart b/packages/devtools_test/lib/src/helpers/wrappers.dart
index 20f16ad..a782b26 100644
--- a/packages/devtools_test/lib/src/helpers/wrappers.dart
+++ b/packages/devtools_test/lib/src/helpers/wrappers.dart
@@ -96,6 +96,7 @@
ReleaseNotesController? releaseNotes,
VMDeveloperToolsController? vmDeveloperTools,
DTDToolsController? dtdTools,
+ AccessibilityController? accessibility,
bool includeRouter = true,
DevToolsQueryParams? queryParams,
}) {
@@ -173,6 +174,12 @@
offline: offline,
);
}
+ if (accessibility != null) {
+ screenControllers.register<AccessibilityController>(
+ () => accessibility,
+ offline: offline,
+ );
+ }
var child = wrapWithNotifications(widget);
final providers = [