Skip to content

TextSpan has no linkUrl, so a tappable span becomes a link with no href that cannot be focused #194551

Open

Description

@jorgelrj

Reproduced on Flutter 3.47.4 stable (9584c67), Chrome on macOS. The code involved is unchanged on master 697012a (links below).

Use case

A TextSpan with a TapGestureRecognizer becomes a semantics node with isLink = true, but there is no way to say where the link goes. On web, the engine renders it as an <a> with no href. Such an element is not in the Tab order, a screen reader cannot treat it as a visitable link, and the browser's context menu does not offer to open it in a new tab. In a real app, inline links inside running text (for example "Read the guide today.") are the common case, and they cannot get a real href today.

This is closer to a proposal than a bug: TextSpan has no way to carry a URL.

Code sample:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Scaffold(
      body: Text.rich(
        TextSpan(
          children: [
            const TextSpan(text: 'Read '),
            TextSpan(text: 'the guide', recognizer: TapGestureRecognizer()..onTap = () {}),
            const TextSpan(text: ' today.'),
          ],
        ),
      ),
    ),
  ),
);

Steps:

  1. Build the sample for web, serve it, and enable semantics (activate the placeholder with document.querySelector('flt-semantics-placeholder').click(), or use a screen reader).
  2. Inspect the link's element and the accessibility tree.

Observed DOM and Chrome accessibility tree:

DOM <a id="flt-semantic-node-6" flt-tappable="" style="display: block; …">the guide</a>
AX  link "the guide"            (no url property)

On the framework side, the widget test below prints:

link node: isLink=true linkUrl=null isFocused=Tristate.none actions=1

actions=1 is SemanticsAction.tap alone. The node has no linkUrl, and isFocused is Tristate.none, so it is not focusable.

Widget test that prints the framework node

package:repro/span_link.dart is the code sample above.

import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:repro/span_link.dart' as link;

void main() {
  testWidgets('span link', (tester) async {
    final h = tester.ensureSemantics();
    link.main();
    await tester.pump();
    final linkNode = find.semantics.byLabel('the guide').evaluate().single.getSemanticsData();
    debugPrint('link node: isLink=${linkNode.flagsCollection.isLink} linkUrl=${linkNode.linkUrl} isFocused=${linkNode.flagsCollection.isFocused} actions=${linkNode.actions}');
    h.dispose();
  });
}

Playwright's getByRole('link') does not match the element, because an <a> without href has no implicit link role in its ARIA computation. Chrome's accessibility tree still reports link.

Where it comes from (unchanged on master):

Today the only way to get a real href is a WidgetSpan whose child is MergeSemantics(Semantics(link: true, linkUrl: …, child: …)). This is the shape url_launcher_web's Link uses (url_launcher_web/lib/src/link.dart L119-L123). It costs line wrapping inside the label, and the paragraph must be assembled per span to keep the reading order: when we tried it, that needed a semanticsIdentifier on a plain sibling span.

Proposal

Add Uri? linkUrl to TextSpan (and to InlineSpanSemanticsInformation), and in RenderParagraph's per-span semantics assembly set configuration.linkUrl, plus isFocusable when there is a tap handler. On web, SemanticLink would then write the href, and the link would be focusable, reachable with Tab, and announced as a visitable link.

Related issues

Activity

  1. added
    in triagePresently being triaged by the triage team
    platform-webWeb applications specifically
    c: proposalA detailed proposal for a change to Flutter
    workaround availableThere is a workaround available to overcome the issue
    and removed
    in triagePresently being triaged by the triage team
    on Oct 16, 2026
  2. puneetkukreja98 commented on Oct 16, 2026

    @puneetkukreja98
    Contributor

    @jorgelrj Thanks for the proposal, I verified this on flutter web, the generated anchor tag indeed lacks the href attribute.

    Image
  3. added
    fyi-webFor the attention of Web platform team
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    platform-webWeb applications specifically
    workaround availableThere is a workaround available to overcome the issue
    and removed
    platform-webWeb applications specifically
    workaround availableThere is a workaround available to overcome the issue
    fyi-webFor the attention of Web platform team
    on Oct 16, 2026
  4. added
    P2Important issues not at the top of the work list
    triaged-webTriaged by Web platform team
    on Oct 21, 2026
  5. flutter-zl commented on Oct 21, 2026

    @flutter-zl
    Contributor
    • Demo: https://flutter-demo-10-before.web.app, fourth section. A TextSpan with a TapGestureRecognizer next to a WidgetSpan link with linkUrl as the control.
      • Steps: Tab through the page, then run [...document.querySelectorAll('a')].map((a) => [a.textContent, a.getAttribute('href')]) in the console.
      • Bug: Tab skips "the guide" and lands on the WidgetSpan link, and the span's <a> has href null. Fixed: the span link is in the Tab order and has a real href.
  6. added
    team-webOwned by Web platform team
    triaged-webTriaged by Web platform team
    and removed
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)c: proposalA detailed proposal for a change to Flutterplatform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform teamworkaround availableThere is a workaround available to overcome the issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions