Repository navigation
Description
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:
- 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). - 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):
painting/inline_span.dartL58-L103:InlineSpanSemanticsInformationcarries text, label, identifier, string attributes and a recognizer, but no URL.painting/text_span.darthas nolinkUrleither.rendering/paragraph.dartL1419-L1425 (3.47.4), L1435-L1441 on master: a span with aTapGestureRecognizerorDoubleTapGestureRecognizergetsonTapandisLink = true. Nothing can setlinkUrl, and the node is not marked focusable.- On the engine side,
semantics/link.dartL30-L36:SemanticLinksetshrefonly whenhasLinkUrl.
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
- [Google3 Bug]: [a11y] [Android] Talkback can't focus text sections with inline links on Android mobile web #174712 (open): TalkBack cannot focus text sections that have inline links, on mobile web.
- SemanticLink causes full page navigation for same-origin URLs on Flutter Web #183620 (closed):
SemanticLinkcauses a full page navigation for same-origin URLs.
Activity
puneetkukreja98 commentedon Oct 16, 2026
@jorgelrj Thanks for the proposal, I verified this on flutter web, the generated anchor tag indeed lacks the href attribute.
flutter-zl commentedon Oct 21, 2026
TextSpanwith aTapGestureRecognizernext to aWidgetSpanlink withlinkUrlas the control.[...document.querySelectorAll('a')].map((a) => [a.textContent, a.getAttribute('href')])in the console.WidgetSpanlink, and the span's<a>hashrefnull. Fixed: the span link is in the Tab order and has a realhref.