iOS Accessibility Issue: StoryBlock in Column Layout

Problem

When StoryBlock is placed in a Column with other widgets (like Text), iOS VoiceOver accessibility fails:

  • VoiceOver cannot interact with StoryBlock properly

  • Tapping StoryBlock reads the wrong content (e.g., the Text label)

circle-info

In this problem scenario, when iOS VoiceOver is enabled, although elements within the StoryBlock—such as the Play/Pause button—are not selectable or readable through direct tap, they remain fully accessible and can be navigated and read using horizontal swipe gestures, thereby maintaining full compliance with WCAG Accessibility standards.

Root Cause

This is a Flutter framework limitation on iOS when combining Column layouts with Platform Views, not a StoryBlock bug. Flutter's Column widget has semantic tree issues with Platform Views specifically on iOS:

  1. Semantic Node Presence: When a widget before StoryBlock creates a semantic node (like Text), it interferes with StoryBlock's accessibility on iOS

  2. Semantic Merging: Column merges child semantics, causing the Text label to dominate and obscure StoryBlock's interactive elements

Note: The exact mechanism is complex and may involve coordinate transformation issues in Flutter's iOS accessibility bridge. The empirical finding is clear: any semantic node before a Platform View in a Column causes the problem on iOS. This affects UiKitView on iOS. Android (PlatformViewLink) is not affected.

Solution

Use ListView instead of Column:

// ❌ DON'T: Using Column
Column(
  children: [
    Text("Story Block Title"),
    StoryBlock(...),
  ],
)

// ✅ DO: Using ListView
ListView(
  shrinkWrap: true,                              // Fits content like Column
  physics: const NeverScrollableScrollPhysics(), // Disables scrolling
  children: [
    Text("Story Block Title"),
    StoryBlock(...),
  ],
)

Why ListView Works

ListView provides semantic isolation through Flutter's Sliver protocol:

  • Each child has an independent coordinate system (no cumulative offsets)

  • No automatic semantic merging

  • Clear accessibility boundaries

Parameters Explained

  • shrinkWrap: true - Makes ListView fit its content height (behaves like Column)

  • physics: NeverScrollableScrollPhysics() - Disables scrolling to avoid gesture conflicts

If ListView is not feasible, use ExcludeSemantics to hide other widgets from accessibility:

Trade-off: The wrapped widget becomes invisible to screen readers.

Last updated

Was this helpful?