[A11y] Add Semantics tree node details (#10018)
diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart
index 5fd07a4..a2e566a 100644
--- a/packages/devtools_app/lib/devtools_app.dart
+++ b/packages/devtools_app/lib/devtools_app.dart
@@ -12,6 +12,7 @@
 export 'src/framework/scaffold/status_line.dart';
 export 'src/screens/accessibility/accessibility_controller.dart';
 export 'src/screens/accessibility/accessibility_screen.dart';
+export 'src/screens/accessibility/semantics_node_details_pane.dart';
 export 'src/screens/accessibility/semantics_node_model.dart';
 export 'src/screens/app_size/app_size_controller.dart';
 export 'src/screens/app_size/app_size_screen.dart';
diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
index 573756a..df02423 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
@@ -55,6 +55,11 @@
     addAutoDisposeListener(boldText, _onBoldTextChanged);
     addAutoDisposeListener(screenReader, _onScreenReaderChanged);
     addAutoDisposeListener(highContrast, _onHighContrastChanged);
+    addAutoDisposeListener(semanticsRoots, _onSemanticsRootsChanged);
+  }
+
+  void _onSemanticsRootsChanged() {
+    selectSemanticsNode(semanticsRoots.value.firstOrNull);
   }
 
   void _initSemanticsTree() {
@@ -148,10 +153,19 @@
   final highContrast = ValueNotifier<bool>(false);
 
   final semanticsRoots = ValueNotifier<List<SemanticsNodeModel>>([]);
+  final selectedSemanticsNode = ValueNotifier<SemanticsNodeModel?>(null);
   final semanticsTreeLoading = ValueNotifier<bool>(false);
   final semanticsTreeError = ValueNotifier<String?>(null);
   final treeScrollController = ScrollController();
 
+  /// Selects [node] in the semantics tree and unselects any previously selected node.
+  void selectSemanticsNode(SemanticsNodeModel? node) {
+    if (selectedSemanticsNode.value == node) return;
+    selectedSemanticsNode.value?.unselect();
+    node?.select();
+    selectedSemanticsNode.value = node;
+  }
+
   Future<void> loadSemanticsTree() async {
     if (semanticsTreeLoading.value) return;
 
@@ -175,6 +189,8 @@
       final response = await serviceConnection.serviceManager
           .callServiceExtensionOnMainIsolate(registrations.getSemanticsTree);
 
+      if (disposed) return;
+
       final json = response.json;
       if (json != null && json.containsKey('error')) {
         throw Exception(json['error']);
@@ -208,8 +224,10 @@
       semanticsTreeError.value = null;
     } catch (e, st) {
       _log.warning('Error loading semantics tree: $e', e, st);
-      semanticsRoots.value = [];
-      semanticsTreeError.value = 'Failed to load semantics tree: $e';
+      if (!disposed) {
+        semanticsRoots.value = [];
+        semanticsTreeError.value = 'Failed to load semantics tree: $e';
+      }
     } finally {
       if (!disposed) {
         semanticsTreeLoading.value = false;
@@ -253,10 +271,14 @@
   SemanticsNodeModel _parseSemanticsNode(Map<String, dynamic> json) {
     final rawFlags = json['flags'] as List<Object?>?;
     final flags = SemanticsNodeModel.parseFlags(rawFlags);
+    final rect = SemanticsNodeModel.parseRect(json['rect']);
 
     return SemanticsNodeModel(
       id: json['id']?.toString() ?? '',
       label: json['label']?.toString() ?? '',
+      value: json['value']?.toString() ?? '',
+      hint: json['hint']?.toString() ?? '',
+      rect: rect,
       flags: flags,
       widgetName: json['widgetName']?.toString() ?? '',
     );
@@ -271,6 +293,7 @@
     screenReader.dispose();
     highContrast.dispose();
     semanticsRoots.dispose();
+    selectedSemanticsNode.dispose();
     semanticsTreeLoading.dispose();
     semanticsTreeError.dispose();
     treeScrollController.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
index f32e71e..89e47cc 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
@@ -10,6 +10,7 @@
 import '../../shared/globals.dart';
 import 'accessibility_controller.dart';
 import 'overrides_pane.dart';
+import 'semantics_node_details_pane.dart';
 import 'semantics_tree_pane.dart';
 
 export 'overrides_pane.dart';
@@ -37,6 +38,12 @@
   // ignore: unused-code, temporarily ignore since this screen is under active development.
   late AccessibilityController controller;
 
+  /// The initial split fractions for the three screen panes:
+  /// - [AccessibilitySemanticsTreePane]: 35%
+  /// - [SemanticsNodeDetailsPane]: 35%
+  /// - [AccessibilityOverridesPane]: 30%
+  static const _initialFractions = [0.35, 0.35, 0.3];
+
   @override
   void initState() {
     super.initState();
@@ -49,9 +56,10 @@
     final splitAxis = _splitAxisFor(context);
     return SplitPane(
       axis: splitAxis,
-      initialFractions: const [0.6, 0.4],
+      initialFractions: _initialFractions,
       children: const [
         AccessibilitySemanticsTreePane(),
+        SemanticsNodeDetailsPane(),
         AccessibilityOverridesPane(),
       ],
     );
diff --git a/packages/devtools_app/lib/src/screens/accessibility/semantics_node_details_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/semantics_node_details_pane.dart
new file mode 100644
index 0000000..7202781
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/accessibility/semantics_node_details_pane.dart
@@ -0,0 +1,250 @@
+// 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 'dart:ui' show SemanticsFlag;
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:material_ui/material_ui.dart';
+
+import '../../shared/globals.dart';
+import '../../shared/ui/common_widgets.dart';
+import 'accessibility_controller.dart';
+import 'semantics_node_model.dart';
+
+/// A pane that displays the details of the currently selected semantics node.
+class SemanticsNodeDetailsPane extends StatelessWidget {
+  const SemanticsNodeDetailsPane({super.key});
+
+  static const _paneTitle = 'Semantics Node Details';
+  static const _emptyMessage =
+      'Select a node in the semantics tree to view its details.';
+
+  @override
+  Widget build(BuildContext context) {
+    final controller = screenControllers.lookup<AccessibilityController>();
+    return DevToolsAreaPane(
+      header: const AreaPaneHeader(
+        title: Text(_paneTitle),
+        roundedTopBorder: false,
+        includeTopBorder: false,
+      ),
+      child: ValueListenableBuilder<SemanticsNodeModel?>(
+        valueListenable: controller.selectedSemanticsNode,
+        builder: (context, selectedNode, _) {
+          if (selectedNode == null) {
+            return const CenteredMessage(message: _emptyMessage);
+          }
+          return _SemanticsNodeDetailsContent(node: selectedNode);
+        },
+      ),
+    );
+  }
+}
+
+/// Displays the properties and flags of a selected [SemanticsNodeModel].
+class _SemanticsNodeDetailsContent extends StatelessWidget {
+  const _SemanticsNodeDetailsContent({required this.node});
+
+  final SemanticsNodeModel node;
+
+  static const _labelTitle = 'label';
+  static const _labelDescription = 'The screen reader announcement text.';
+
+  static const _valueTitle = 'value';
+  static const _valueDescription =
+      'The current value of a progress indicator, slider, etc.';
+
+  static const _hintTitle = 'hint';
+  static const _hintDescription =
+      'A brief description of the action that will occur.';
+
+  static const _rectTitle = 'rect';
+  static const _rectDescription =
+      'The bounding box of the node in logical pixels.';
+
+  static const _flagsTitle = 'Flags';
+  static const _flagsDescription =
+      'Boolean flags that dictate the semantics behavior.';
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Scrollbar(
+      child: SingleChildScrollView(
+        padding: const EdgeInsets.all(defaultSpacing),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text('SemanticsNode #${node.id}', style: theme.boldTextStyle),
+            const SizedBox(height: denseSpacing),
+            const Divider(),
+            const SizedBox(height: denseSpacing),
+            _NodeDetailSection(
+              title: _labelTitle,
+              description: _labelDescription,
+              child: _NodeDetailValueBox(
+                text: node.label,
+                highlightText: true,
+                wrapInQuotes: true,
+              ),
+            ),
+            const SizedBox(height: defaultSpacing),
+            _NodeDetailSection(
+              title: _valueTitle,
+              description: _valueDescription,
+              child: _NodeDetailValueBox(text: node.value, wrapInQuotes: true),
+            ),
+            const SizedBox(height: defaultSpacing),
+            _NodeDetailSection(
+              title: _hintTitle,
+              description: _hintDescription,
+              child: _NodeDetailValueBox(text: node.hint, wrapInQuotes: true),
+            ),
+            const SizedBox(height: defaultSpacing),
+            _NodeDetailSection(
+              title: _rectTitle,
+              description: _rectDescription,
+              child: _NodeDetailValueBox(text: node.rectDisplay),
+            ),
+            const SizedBox(height: defaultSpacing),
+            _NodeDetailSection(
+              title: _flagsTitle,
+              description: _flagsDescription,
+              child: node.flags.isEmpty
+                  ? const _NodeDetailValueBox(text: null)
+                  : _SemanticsFlagsWrap(flags: node.flags),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// A labeled section in the node details view with a title, description, and content.
+class _NodeDetailSection extends StatelessWidget {
+  const _NodeDetailSection({
+    required this.title,
+    required this.description,
+    required this.child,
+  });
+
+  final String title;
+  final String description;
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(title, style: theme.boldTextStyle),
+        const SizedBox(height: densePadding),
+        Text(description, style: theme.subtleTextStyle),
+        const SizedBox(height: denseSpacing),
+        child,
+      ],
+    );
+  }
+}
+
+/// A bordered container that displays a property value or `(empty)` if none is present.
+class _NodeDetailValueBox extends StatelessWidget {
+  const _NodeDetailValueBox({
+    required this.text,
+    this.highlightText = false,
+    this.wrapInQuotes = false,
+  });
+
+  final String? text;
+  final bool highlightText;
+  final bool wrapInQuotes;
+
+  static const _emptyPlaceholder = '(empty)';
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final colorScheme = theme.colorScheme;
+    final hasValue = text != null && text!.isNotEmpty;
+    final displayText = hasValue
+        ? (wrapInQuotes ? '"$text"' : text!)
+        : _emptyPlaceholder;
+
+    return Container(
+      width: double.infinity,
+      padding: const EdgeInsets.all(defaultSpacing),
+      decoration: BoxDecoration(
+        color: colorScheme.onSurface.withValues(alpha: 0.05),
+        borderRadius: defaultBorderRadius,
+        border: Border.all(
+          color: highlightText && hasValue
+              ? colorScheme.primary.withValues(alpha: 0.5)
+              : theme.focusColor,
+        ),
+      ),
+      child: SelectableText(
+        displayText,
+        style: _textStyle(theme, hasValue: hasValue),
+      ),
+    );
+  }
+
+  TextStyle _textStyle(ThemeData theme, {required bool hasValue}) {
+    if (!hasValue) {
+      return theme.subtleFixedFontStyle;
+    }
+    if (highlightText) {
+      return theme.fixedFontStyle.copyWith(
+        color: theme.colorScheme.primary,
+        fontWeight: FontWeight.bold,
+      );
+    }
+    return theme.fixedFontStyle;
+  }
+}
+
+/// A wrapping layout of chips representing active [SemanticsFlag]s on a node.
+class _SemanticsFlagsWrap extends StatelessWidget {
+  const _SemanticsFlagsWrap({required this.flags});
+
+  final Set<SemanticsFlag> flags;
+
+  @override
+  Widget build(BuildContext context) {
+    return Wrap(
+      spacing: denseSpacing,
+      runSpacing: denseSpacing,
+      children: [for (final flag in flags) _SemanticsFlagChip(flag: flag)],
+    );
+  }
+}
+
+/// A chip widget displaying the name of a single [SemanticsFlag].
+class _SemanticsFlagChip extends StatelessWidget {
+  const _SemanticsFlagChip({required this.flag});
+
+  final SemanticsFlag flag;
+
+  static const _chipBorderRadius = 4.0;
+  static const _chipBackgroundAlpha = 0.12;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final colorScheme = theme.colorScheme;
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        horizontal: denseSpacing,
+        vertical: densePadding,
+      ),
+      decoration: BoxDecoration(
+        color: colorScheme.onSurface.withValues(alpha: _chipBackgroundAlpha),
+        borderRadius: BorderRadius.circular(_chipBorderRadius),
+      ),
+      child: Text(flag.name, style: theme.fixedFontStyle),
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/accessibility/semantics_node_model.dart b/packages/devtools_app/lib/src/screens/accessibility/semantics_node_model.dart
index 2ee7209..f03fe12 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/semantics_node_model.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/semantics_node_model.dart
@@ -5,7 +5,7 @@
 /// @docImport 'package:flutter/semantics.dart';
 library;
 
-import 'dart:ui' show SemanticsFlag;
+import 'dart:ui' show Rect, SemanticsFlag;
 
 import '../../shared/primitives/trees.dart';
 
@@ -14,6 +14,9 @@
   SemanticsNodeModel({
     required this.id,
     this.label = '',
+    this.value = '',
+    this.hint = '',
+    this.rect,
     this.flags = const <SemanticsFlag>{},
     this.widgetName = '',
   });
@@ -24,6 +27,15 @@
   /// The user-visible label announced by screen readers (maps to [SemanticsData.label]).
   final String label;
 
+  /// The textual description of the value of the node (maps to [SemanticsData.value]).
+  final String value;
+
+  /// A brief description of the result of performing an action on the node (maps to [SemanticsData.hint]).
+  final String hint;
+
+  /// The bounding box of the node in logical pixels (maps to [SemanticsNode.rect]).
+  final Rect? rect;
+
   /// Semantic flags active on this node.
   final Set<SemanticsFlag> flags;
 
@@ -44,11 +56,56 @@
         .toSet();
   }
 
+  /// Parses a JSON representation of a bounding box into a [Rect].
+  static Rect? parseRect(Object? rawRect) {
+    if (rawRect is! Map) return null;
+    final left = (rawRect['left'] as num?)?.toDouble();
+    final top = (rawRect['top'] as num?)?.toDouble();
+    if (left == null || top == null) return null;
+
+    if (rawRect.containsKey('width') && rawRect.containsKey('height')) {
+      final width = (rawRect['width'] as num?)?.toDouble();
+      final height = (rawRect['height'] as num?)?.toDouble();
+      if (width != null && height != null) {
+        return Rect.fromLTWH(left, top, width, height);
+      }
+    }
+    if (rawRect.containsKey('right') && rawRect.containsKey('bottom')) {
+      final right = (rawRect['right'] as num?)?.toDouble();
+      final bottom = (rawRect['bottom'] as num?)?.toDouble();
+      if (right != null && bottom != null) {
+        return Rect.fromLTRB(left, top, right, bottom);
+      }
+    }
+    return null;
+  }
+
+  /// Formatted string representation of [rect] in `Rect.fromLTWH` syntax.
+  String? get rectDisplay {
+    final currentRect = rect;
+    if (currentRect == null) return null;
+    final left = _formatNumber(currentRect.left);
+    final top = _formatNumber(currentRect.top);
+    final width = _formatNumber(currentRect.width);
+    final height = _formatNumber(currentRect.height);
+    return 'rect: Rect.fromLTWH($left, $top, $width, $height)';
+  }
+
+  static String _formatNumber(double value) {
+    if (value == value.truncateToDouble()) {
+      return value.toInt().toString();
+    }
+    return value.toStringAsFixed(1);
+  }
+
   @override
   SemanticsNodeModel shallowCopy() {
     return SemanticsNodeModel(
       id: id,
       label: label,
+      value: value,
+      hint: hint,
+      rect: rect,
       flags: flags,
       widgetName: widgetName,
     );
diff --git a/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart
index 4e58065..3974b15 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart
@@ -167,67 +167,74 @@
     final theme = Theme.of(context);
     final colorScheme = theme.colorScheme;
 
-    return TreeView<SemanticsNodeModel>(
-      dataRootsListenable: controller.semanticsRoots,
-      scrollController: controller.treeScrollController,
-      includeScrollbar: true,
-      dataDisplayProvider: (node, onPressed) {
-        return InkWell(
-          onTap: onPressed,
-          child: Padding(
-            padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
-            child: Row(
-              children: [
-                Icon(
-                  _iconForNode(node),
-                  size: defaultIconSize,
-                  color: colorScheme.onSurface.withValues(alpha: 0.7),
-                ),
-                const SizedBox(width: denseSpacing),
-                Text(
-                  'SemanticsNode #${node.id}',
-                  maxLines: 1,
-                  style: theme.fixedFontStyle,
-                ),
-                if (node.label.isNotEmpty) ...[
-                  const SizedBox(width: denseSpacing),
-                  Flexible(
-                    child: Text(
-                      '"${node.label}"',
+    return ValueListenableBuilder<SemanticsNodeModel?>(
+      valueListenable: controller.selectedSemanticsNode,
+      builder: (context, _, _) {
+        return TreeView<SemanticsNodeModel>(
+          dataRootsListenable: controller.semanticsRoots,
+          scrollController: controller.treeScrollController,
+          includeScrollbar: true,
+          onItemSelected: controller.selectSemanticsNode,
+          onItemExpanded: (_) {},
+          dataDisplayProvider: (node, onPressed) {
+            return InkWell(
+              onTap: onPressed,
+              child: Padding(
+                padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+                child: Row(
+                  children: [
+                    Icon(
+                      _iconForNode(node),
+                      size: defaultIconSize,
+                      color: colorScheme.onSurface.withValues(alpha: 0.7),
+                    ),
+                    const SizedBox(width: denseSpacing),
+                    Text(
+                      'SemanticsNode #${node.id}',
                       maxLines: 1,
-                      overflow: TextOverflow.ellipsis,
-                      style: theme.subtleTextStyle.copyWith(
-                        fontStyle: FontStyle.italic,
-                        color: colorScheme.onSurface.withValues(alpha: 0.6),
+                      style: theme.fixedFontStyle,
+                    ),
+                    if (node.label.isNotEmpty) ...[
+                      const SizedBox(width: denseSpacing),
+                      Flexible(
+                        child: Text(
+                          '"${node.label}"',
+                          maxLines: 1,
+                          overflow: TextOverflow.ellipsis,
+                          style: theme.subtleTextStyle.copyWith(
+                            fontStyle: FontStyle.italic,
+                            color: colorScheme.onSurface.withValues(alpha: 0.6),
+                          ),
+                        ),
                       ),
-                    ),
-                  ),
-                ],
-                if (node.widgetName.isNotEmpty) ...[
-                  const SizedBox(width: denseSpacing),
-                  Container(
-                    padding: const EdgeInsets.symmetric(
-                      horizontal: densePadding,
-                    ),
-                    decoration: BoxDecoration(
-                      color: colorScheme.primaryContainer.withValues(
-                        alpha: 0.2,
+                    ],
+                    if (node.widgetName.isNotEmpty) ...[
+                      const SizedBox(width: denseSpacing),
+                      Container(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: densePadding,
+                        ),
+                        decoration: BoxDecoration(
+                          color: colorScheme.primaryContainer.withValues(
+                            alpha: 0.2,
+                          ),
+                          borderRadius: BorderRadius.circular(4),
+                        ),
+                        child: Text(
+                          node.widgetName,
+                          maxLines: 1,
+                          style: theme.subtleTextStyle.copyWith(
+                            color: colorScheme.primary,
+                            fontSize: smallFontSize,
+                          ),
+                        ),
                       ),
-                      borderRadius: BorderRadius.circular(4),
-                    ),
-                    child: Text(
-                      node.widgetName,
-                      maxLines: 1,
-                      style: theme.subtleTextStyle.copyWith(
-                        color: colorScheme.primary,
-                        fontSize: smallFontSize,
-                      ),
-                    ),
-                  ),
-                ],
-              ],
-            ),
-          ),
+                    ],
+                  ],
+                ),
+              ),
+            );
+          },
         );
       },
     );
diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart
index 06f3eb8..a39b9f5 100644
--- a/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart
+++ b/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart
@@ -5,7 +5,7 @@
 @TestOn('vm')
 library;
 
-import 'dart:ui' show SemanticsFlag;
+import 'dart:ui' show Rect, SemanticsFlag;
 
 import 'package:devtools_app/devtools_app.dart';
 import 'package:devtools_app/src/service/service_registrations.dart'
@@ -144,33 +144,66 @@
       },
     );
 
-    test('SemanticsNodeModel properties and shallowCopy', () {
-      final child = SemanticsNodeModel(
-        id: '1',
-        label: 'Child Node',
-        flags: {SemanticsFlag.isButton, SemanticsFlag.hasCheckedState},
-        widgetName: 'ElevatedButton',
-      );
-      final parent = SemanticsNodeModel(
-        id: '0',
-        label: 'Parent Node',
-        flags: {SemanticsFlag.isHeader},
-        widgetName: 'Column',
-      )..addChild(child);
+    test(
+      'SemanticsNodeModel properties, parseRect, rectDisplay, and shallowCopy',
+      () {
+        const expectedRect = Rect.fromLTWH(0, 0, 763, 32);
+        final child = SemanticsNodeModel(
+          id: '1',
+          label: 'Child Node',
+          value: 'Child Value',
+          hint: 'Child Hint',
+          rect: expectedRect,
+          flags: {SemanticsFlag.isButton, SemanticsFlag.hasCheckedState},
+          widgetName: 'ElevatedButton',
+        );
+        final parent = SemanticsNodeModel(
+          id: '0',
+          label: 'Parent Node',
+          flags: {SemanticsFlag.isHeader},
+          widgetName: 'Column',
+        )..addChild(child);
 
-      expect(parent.children, hasLength(1));
-      expect(parent.children.first.id, equals('1'));
+        expect(parent.children, hasLength(1));
+        expect(parent.children.first.id, equals('1'));
+        expect(parent.rectDisplay, isNull);
+        expect(child.rectDisplay, equals('rect: Rect.fromLTWH(0, 0, 763, 32)'));
 
-      final copy = child.shallowCopy();
-      expect(copy.id, equals('1'));
-      expect(copy.label, equals('Child Node'));
-      expect(
-        copy.flags,
-        equals({SemanticsFlag.isButton, SemanticsFlag.hasCheckedState}),
-      );
-      expect(copy.widgetName, equals('ElevatedButton'));
-      expect(copy.children, isEmpty);
-    });
+        final copy = child.shallowCopy();
+        expect(copy.id, equals('1'));
+        expect(copy.label, equals('Child Node'));
+        expect(copy.value, equals('Child Value'));
+        expect(copy.hint, equals('Child Hint'));
+        expect(copy.rect, equals(expectedRect));
+        expect(
+          copy.flags,
+          equals({SemanticsFlag.isButton, SemanticsFlag.hasCheckedState}),
+        );
+        expect(copy.widgetName, equals('ElevatedButton'));
+        expect(copy.children, isEmpty);
+
+        expect(
+          SemanticsNodeModel.parseRect({
+            'left': 10,
+            'top': 20,
+            'width': 100,
+            'height': 50,
+          }),
+          equals(const Rect.fromLTWH(10, 20, 100, 50)),
+        );
+        expect(
+          SemanticsNodeModel.parseRect({
+            'left': 10,
+            'top': 20,
+            'right': 110,
+            'bottom': 70,
+          }),
+          equals(const Rect.fromLTRB(10, 20, 110, 70)),
+        );
+        expect(SemanticsNodeModel.parseRect(null), isNull);
+        expect(SemanticsNodeModel.parseRect('invalid'), isNull);
+      },
+    );
 
     test(
       'loadSemanticsTree sets error state when no main isolate connected',
@@ -187,6 +220,7 @@
         );
         expect(testController.semanticsTreeLoading.value, isFalse);
         expect(testController.semanticsRoots.value, isEmpty);
+        expect(testController.selectedSemanticsNode.value, isNull);
       },
     );
 
@@ -214,11 +248,12 @@
           ),
         );
         expect(testController.semanticsRoots.value, isEmpty);
+        expect(testController.selectedSemanticsNode.value, isNull);
       },
     );
 
     test(
-      'loadSemanticsTree parses full SemanticsNode.toJson format with multiple nodes',
+      'loadSemanticsTree parses full SemanticsNode.toJson format with multiple nodes and selects root',
       () async {
         final fakeServiceManager =
             serviceConnection.serviceManager as FakeServiceManager;
@@ -318,16 +353,24 @@
         final root = testController.semanticsRoots.value.first;
         expect(root.id, equals('0'));
         expect(root.label, equals('Root View'));
+        expect(root.value, equals('Main Screen'));
+        expect(root.hint, isEmpty);
+        expect(root.rect, equals(const Rect.fromLTWH(0, 0, 390, 844)));
         expect(
           root.flags,
           equals({SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled}),
         );
         expect(root.children, hasLength(2));
 
+        // Root node is automatically selected on load
+        expect(testController.selectedSemanticsNode.value, equals(root));
+        expect(root.isSelected, isTrue);
+
         // Node 1: Settings Header
         final headerNode = root.children[0];
         expect(headerNode.id, equals('1'));
         expect(headerNode.label, equals('Settings Header'));
+        expect(headerNode.rect, equals(const Rect.fromLTWH(16, 40, 358, 32)));
         expect(headerNode.flags, equals({SemanticsFlag.isHeader}));
         expect(headerNode.children, isEmpty);
 
@@ -335,6 +378,9 @@
         final searchNode = root.children[1];
         expect(searchNode.id, equals('2'));
         expect(searchNode.label, equals('Search Input'));
+        expect(searchNode.value, equals('Flutter'));
+        expect(searchNode.hint, equals('Enter search query'));
+        expect(searchNode.rect, equals(const Rect.fromLTWH(16, 88, 358, 48)));
         expect(searchNode.flags, equals({SemanticsFlag.isTextField}));
         expect(searchNode.children, hasLength(1));
 
@@ -343,10 +389,23 @@
         expect(clearButtonNode.id, equals('3'));
         expect(clearButtonNode.label, equals('Clear Text'));
         expect(
+          clearButtonNode.rect,
+          equals(const Rect.fromLTWH(330, 96, 32, 32)),
+        );
+        expect(
           clearButtonNode.flags,
           equals({SemanticsFlag.isButton, SemanticsFlag.hasCheckedState}),
         );
         expect(clearButtonNode.children, isEmpty);
+
+        // Selecting another node updates selectedSemanticsNode and isSelected state
+        testController.selectSemanticsNode(clearButtonNode);
+        expect(
+          testController.selectedSemanticsNode.value,
+          equals(clearButtonNode),
+        );
+        expect(clearButtonNode.isSelected, isTrue);
+        expect(root.isSelected, isFalse);
       },
     );
 
diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
index 37ecbb3..e13b494 100644
--- a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
+++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
@@ -72,11 +72,14 @@
       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);
+
+      // Semantics Node Details pane should be visible
+      expect(find.byType(SemanticsNodeDetailsPane), findsOneWidget);
+
+      // Overrides pane should be visible
+      expect(find.byType(AccessibilityOverridesPane), findsOneWidget);
     });
 
     testWidgetsWithWindowSize(
@@ -245,9 +248,27 @@
         await pumpAccessibilityScreen(tester);
         await tester.pumpAndSettle();
 
-        expect(find.text('SemanticsNode #0'), findsOneWidget);
-        expect(find.text('SemanticsNode #1'), findsOneWidget);
-        expect(find.text('"Child Node"'), findsOneWidget);
+        expect(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('SemanticsNode #0'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('SemanticsNode #1'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('"Child Node"'),
+          ),
+          findsOneWidget,
+        );
         expect(find.text('ElevatedButton'), findsOneWidget);
       },
     );
@@ -306,5 +327,189 @@
         expect(find.byIcon(Icons.check_box_outlined), findsOneWidget);
       },
     );
+
+    testWidgetsWithWindowSize(
+      'renders node details placeholder when no node is selected',
+      windowSize,
+      (WidgetTester tester) async {
+        controller.semanticsRoots.value = [];
+
+        await pumpAccessibilityScreen(tester);
+        await tester.pumpAndSettle();
+
+        expect(find.text('Semantics Node Details'), findsOneWidget);
+        expect(
+          find.text('Select a node in the semantics tree to view its details.'),
+          findsOneWidget,
+        );
+      },
+    );
+
+    testWidgetsWithWindowSize(
+      'renders root node details automatically and updates when a child node is selected',
+      windowSize,
+      (WidgetTester tester) async {
+        final childNode = SemanticsNodeModel(
+          id: '26',
+          label: 'CloseButton',
+          rect: const Rect.fromLTWH(0, 0, 763, 32),
+          flags: {
+            SemanticsFlag.isButton,
+            SemanticsFlag.hasEnabledState,
+            SemanticsFlag.isEnabled,
+            SemanticsFlag.isFocusable,
+          },
+          widgetName: 'CloseButton',
+        );
+        final rootNode = SemanticsNodeModel(
+          id: '0',
+          label: 'Root Node',
+          value: 'Initial Value',
+          hint: 'Tap to interact',
+          rect: const Rect.fromLTWH(0, 0, 1000, 1000),
+          flags: {SemanticsFlag.isHeader},
+        )..addChild(childNode);
+
+        rootNode.expandCascading();
+        controller.semanticsRoots.value = [rootNode];
+
+        await pumpAccessibilityScreen(tester);
+        await tester.pumpAndSettle();
+
+        final detailsPane = find.byType(SemanticsNodeDetailsPane);
+
+        // Initially, root node (#0) is automatically selected
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('SemanticsNode #0'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('"Root Node"')),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('"Initial Value"'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('"Tap to interact"'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('rect: Rect.fromLTWH(0, 0, 1000, 1000)'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('isHeader')),
+          findsOneWidget,
+        );
+
+        // Tap child node (#26) in the tree pane
+        await tester.tap(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('SemanticsNode #26'),
+          ),
+        );
+        await tester.pumpAndSettle();
+
+        // Details pane should now display SemanticsNode #26 details
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('SemanticsNode #26'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('"CloseButton"'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('rect: Rect.fromLTWH(0, 0, 763, 32)'),
+          ),
+          findsOneWidget,
+        );
+        // Empty value and hint should render as (empty)
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('(empty)')),
+          findsNWidgets(2),
+        );
+        // Flags chips
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('isButton')),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(
+            of: detailsPane,
+            matching: find.text('hasEnabledState'),
+          ),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('isEnabled')),
+          findsOneWidget,
+        );
+        expect(
+          find.descendant(of: detailsPane, matching: find.text('isFocusable')),
+          findsOneWidget,
+        );
+
+        // Tap parent node (#0) in the tree pane to select it; its child (#26)
+        // should remain expanded and visible in the tree.
+        await tester.tap(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('SemanticsNode #0'),
+          ),
+        );
+        await tester.pumpAndSettle();
+
+        expect(controller.selectedSemanticsNode.value, equals(rootNode));
+        expect(rootNode.isExpanded, isTrue);
+        expect(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.text('SemanticsNode #26'),
+          ),
+          findsOneWidget,
+        );
+
+        // Select child (#26) again, then tap the expand/collapse arrow on
+        // parent (#0); it should collapse without changing the selected node.
+        controller.selectSemanticsNode(childNode);
+        await tester.pumpAndSettle();
+        expect(controller.selectedSemanticsNode.value, equals(childNode));
+
+        await tester.tap(
+          find.descendant(
+            of: find.byType(AccessibilitySemanticsTreePane),
+            matching: find.byIcon(Icons.keyboard_arrow_down),
+          ),
+        );
+        await tester.pumpAndSettle();
+
+        expect(rootNode.isExpanded, isFalse);
+        expect(controller.selectedSemanticsNode.value, equals(childNode));
+      },
+    );
   });
 }