NakedLink

Headless Flutter link with caller-owned navigation, accessible semantics, and observable interaction state

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.

When to use this

  • 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.

Basic implementation

dart
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!,
    );
  },
)

Navigation ownership

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.

Activation behavior

InputBehavior
Primary tapCalls onPressed once
Enter or Numpad EnterCalls onPressed once; held-key repeats are ignored
Semantic tapCalls the same onPressed path
SpaceRemains unclaimed so the page can scroll
Modified primary clickCalls onPressed; modifier policy is caller-owned
Secondary clickRemains unclaimed

Accessible naming

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.

Constructor

dart
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,
})

Properties

onPressed → VoidCallback?

Performs caller-owned navigation. A null callback makes the Link inert.

linkUrl → Uri?

Optional destination metadata for assistive technologies. On web it also maps to an anchor href while the Link is enabled.

child → Widget?

The visual content. Supply child, builder, or both.

builder → ValueWidgetBuilder<NakedLinkState>?

Builds visuals from an immutable state containing interaction flags and the current linkUrl.

enabled → bool

Allows activation when onPressed is also non-null. Defaults to true.

focusNode → FocusNode?

Optional caller-owned focus node. NakedLink never disposes it.

semanticLabel → String?

Optional localized accessible-name override. Blank values are treated as absent.

semanticHint → String?

Optional localized description of a non-obvious navigation result.

excludeSemantics → bool

Hides the Link and its subtree from accessibility services. Defaults to false.