NakedLink is a dependency-free navigation primitive with no built-in styling
or launching policy. It supplies Link semantics and the correct keyboard
contract while the application owns routing, URL launching, and validation.
- Navigation: Move to a route, document, website, or other destination
- Inline links: Compose navigation into text without button behavior
- Design systems: Render custom visuals from hover, focus, press, and disabled state
Use a button for an in-place action. A Link communicates that activation navigates to or references another destination.
The complete fixture is in the GitHub repository.
NakedLink(
onPressed: () => Navigator.of(context).pushNamed('/docs'),
child: const Text('Documentation'),
builder: (context, state, child) {
return DecoratedBox(
decoration: BoxDecoration(
color: state.isHovered ? const Color(0xFFEFF6FF) : null,
border: Border.all(
color: state.isFocused
? const Color(0xFF2563EB)
: const Color(0x00000000),
),
),
child: child!,
);
},
)onPressed is the only application activation callback. NakedLink does not
call a launcher or choose a router, so applications can use Navigator, a
routing package, or an optional launcher dependency without adding that policy
to every consumer.
linkUrl is optional destination metadata exposed through Flutter semantics.
On Flutter web, a non-null semantics URL becomes an anchor href. Omit
linkUrl when onPressed performs navigation; otherwise one DOM activation
can call the callback and follow the anchor. Callers must validate destinations
before passing them to either API.
Modified primary clicks follow the ordinary onPressed path. If an application
needs browser-style open-in-new-tab behavior, its callback or an opt-in
anchor/launcher layer must implement that policy.
A Link is interactive only when enabled is true and onPressed is non-null.
Otherwise it has no Link role, destination URL, tap action, or focus stop.
| Input | Behavior |
|---|---|
| Primary tap | Calls onPressed once |
| Enter or Numpad Enter | Calls onPressed once; held-key repeats are ignored |
| Semantic tap | Calls the same onPressed path |
| Space | Remains unclaimed so the page can scroll |
| Modified primary click | Calls onPressed; modifier policy is caller-owned |
| Secondary click | Remains unclaimed |
Visible child text supplies the accessible name by default. A non-empty,
caller-localized semanticLabel replaces descendant naming semantics so the
Link is announced once. Null and whitespace-only labels preserve the visible
child name. Use semanticHint only when the result is not clear from the name.
Set excludeSemantics only when the surrounding application supplies an
equivalent accessible navigation path.
const NakedLink({
Key? key,
Widget? child,
ValueWidgetBuilder<NakedLinkState>? builder,
VoidCallback? onPressed,
Uri? linkUrl,
bool enabled = true,
FocusNode? focusNode,
bool autofocus = false,
MouseCursor? mouseCursor,
bool enableFeedback = true,
ValueChanged<bool>? onFocusChange,
ValueChanged<bool>? onHoverChange,
ValueChanged<bool>? onPressChange,
String? semanticLabel,
String? semanticHint,
bool excludeSemantics = false,
})Performs caller-owned navigation. A null callback makes the Link inert.
Optional destination metadata for assistive technologies. On web it also maps
to an anchor href while the Link is enabled.
The visual content. Supply child, builder, or both.
Builds visuals from an immutable state containing interaction flags and the
current linkUrl.
Allows activation when onPressed is also non-null. Defaults to true.
Optional caller-owned focus node. NakedLink never disposes it.
Optional localized accessible-name override. Blank values are treated as absent.
Optional localized description of a non-obvious navigation result.
Hides the Link and its subtree from accessibility services. Defaults to
false.