| null = null;
+
+ for (const item of this.inputs.items()) {
+ if (this.focusManager.isFocusable(item)) {
+ if (!firstItem) {
+ firstItem = item;
+ }
+ if (item.selected()) {
+ this.inputs.activeIndex.set(item.index());
+ return;
+ }
+ }
+ }
+
+ if (firstItem) {
+ this.inputs.activeIndex.set(firstItem.index());
+ }
+ }
+
+ /**
+ * Safely performs a navigation operation.
+ *
+ * Handles boilerplate calling of focus & selection operations. Also ensures these
+ * additional operations are only called if the navigation operation moved focus to a new link.
+ */
+ private _navigate(opts: SelectOptions = {}, operation: () => boolean) {
+ const moved = operation();
+
+ if (moved) {
+ this._updateSelection(opts);
+ }
+ }
+
+ /** Handles updating selection for the nav. */
+ private _updateSelection(opts: SelectOptions = {}) {
+ // In nav, navigation always implies selection (activation).
+ if (opts.selectOne) {
+ this.selection.selectOne();
+ }
+ }
+
+ /** Gets the LinkPattern associated with a pointer event target. */
+ private _getItem(e: PointerEvent) {
+ if (!(e.target instanceof HTMLElement)) {
+ return;
+ }
+
+ // Assuming links have a role or specific attribute to identify them.
+ // Adjust selector as needed based on actual link implementation.
+ const element = e.target.closest('[role="link"], [cdkLink]');
+ return this.inputs.items().find(i => i.element() === element);
+ }
+}
diff --git a/src/cdk-experimental/ui-patterns/public-api.ts b/src/cdk-experimental/ui-patterns/public-api.ts
index 06383ea9b5bf..c8c2d7790f0a 100644
--- a/src/cdk-experimental/ui-patterns/public-api.ts
+++ b/src/cdk-experimental/ui-patterns/public-api.ts
@@ -11,4 +11,6 @@ export * from './listbox/option';
export * from './radio/radio-group';
export * from './radio/radio';
export * from './behaviors/signal-like/signal-like';
+export * from './nav/nav';
+export * from './nav/link';
export * from './tabs/tabs';
diff --git a/src/components-examples/cdk-experimental/nav/BUILD.bazel b/src/components-examples/cdk-experimental/nav/BUILD.bazel
new file mode 100644
index 000000000000..964ead06538e
--- /dev/null
+++ b/src/components-examples/cdk-experimental/nav/BUILD.bazel
@@ -0,0 +1,29 @@
+load("//tools:defaults.bzl", "ng_project")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_project(
+ name = "nav",
+ srcs = glob(["**/*.ts"]),
+ assets = glob([
+ "**/*.html",
+ "**/*.css",
+ ]),
+ deps = [
+ "//:node_modules/@angular/core",
+ "//:node_modules/@angular/forms",
+ "//src/cdk-experimental/nav",
+ "//src/material/checkbox",
+ "//src/material/form-field",
+ "//src/material/select",
+ ],
+)
+
+filegroup(
+ name = "source-files",
+ srcs = glob([
+ "**/*.html",
+ "**/*.css",
+ "**/*.ts",
+ ]),
+)
diff --git a/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.css b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.css
new file mode 100644
index 000000000000..25ec5ed7e624
--- /dev/null
+++ b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.css
@@ -0,0 +1,44 @@
+.example-nav {
+ border: 1px solid var(--mat-sys-outline);
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ gap: 4px; /* Add gap like listbox */
+}
+
+.example-link {
+ padding: 8px 12px;
+ cursor: pointer;
+ text-decoration: none;
+ border: 1px solid var(--mat-sys-outline);
+ border-radius: var(--mat-sys-corner-extra-small);
+ display: block; /* Ensure links take full width */
+ outline: none; /* Remove default browser focus outline */
+ position: relative; /* For potential future pseudo-elements */
+}
+
+/* Style for hover and keyboard focus (cdk-active) */
+.example-link.cdk-active:not([aria-disabled='true']),
+.example-link:hover:not([aria-disabled='true']) {
+ outline: 1px solid var(--mat-sys-outline);
+ background: var(--mat-sys-surface-container);
+}
+
+/* Style for focus-visible (programmatic/keyboard focus) */
+.example-link:focus-visible:not([aria-disabled='true']) {
+ outline: 2px solid var(--mat-sys-primary);
+ background: var(--mat-sys-surface-container);
+}
+
+/* Style for the selected link (using aria-current="page") */
+.example-link[aria-current='page']:not([aria-disabled='true']) {
+ background-color: var(--mat-sys-secondary-container);
+ font-weight: bold;
+}
+
+.example-link[aria-disabled='true'] {
+ color: var(--mat-sys-on-surface);
+ opacity: 0.3;
+ cursor: not-allowed;
+}
diff --git a/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.html b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.html
new file mode 100644
index 000000000000..3772a408a7e1
--- /dev/null
+++ b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.html
@@ -0,0 +1,15 @@
+
+
+
+ Selected Value: {{ selectedValue().join(', ') }}
+
diff --git a/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.ts b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.ts
new file mode 100644
index 000000000000..d0b74776a18a
--- /dev/null
+++ b/src/components-examples/cdk-experimental/nav/cdk-nav/cdk-nav-example.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {ChangeDetectionStrategy, Component, model} from '@angular/core';
+import {CdkLink, CdkNav} from '@angular/cdk-experimental/nav';
+
+@Component({
+ selector: 'cdk-nav-example',
+ templateUrl: 'cdk-nav-example.html',
+ styleUrl: 'cdk-nav-example.css',
+ standalone: true,
+ imports: [CdkNav, CdkLink],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CdkNavExample {
+ selectedValue = model(['/home']); // Default selected value
+
+ links = [
+ {label: 'Home', path: '/home'},
+ {label: 'Settings', path: '/settings'},
+ {label: 'Profile', path: '/profile', disabled: true},
+ {label: 'Admin', path: '/admin'},
+ ];
+}
diff --git a/src/components-examples/cdk-experimental/nav/index.ts b/src/components-examples/cdk-experimental/nav/index.ts
new file mode 100644
index 000000000000..6621377eeb6e
--- /dev/null
+++ b/src/components-examples/cdk-experimental/nav/index.ts
@@ -0,0 +1 @@
+export {CdkNavExample} from './cdk-nav/cdk-nav-example';
diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel
index 8073332cd43f..faaec1cbba0e 100644
--- a/src/dev-app/BUILD.bazel
+++ b/src/dev-app/BUILD.bazel
@@ -35,6 +35,7 @@ ng_project(
"//src/dev-app/cdk-dialog",
"//src/dev-app/cdk-experimental-combobox",
"//src/dev-app/cdk-experimental-listbox",
+ "//src/dev-app/cdk-experimental-nav",
"//src/dev-app/cdk-experimental-tabs",
"//src/dev-app/cdk-listbox",
"//src/dev-app/cdk-menu",
diff --git a/src/dev-app/cdk-experimental-nav/BUILD.bazel b/src/dev-app/cdk-experimental-nav/BUILD.bazel
new file mode 100644
index 000000000000..186f5c886946
--- /dev/null
+++ b/src/dev-app/cdk-experimental-nav/BUILD.bazel
@@ -0,0 +1,13 @@
+load("//tools:defaults.bzl", "ng_project")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_project(
+ name = "cdk-experimental-nav",
+ srcs = glob(["**/*.ts"]),
+ assets = ["cdk-nav-demo.html"],
+ deps = [
+ "//:node_modules/@angular/core",
+ "//src/components-examples/cdk-experimental/nav",
+ ],
+)
diff --git a/src/dev-app/cdk-experimental-nav/cdk-nav-demo.html b/src/dev-app/cdk-experimental-nav/cdk-nav-demo.html
new file mode 100644
index 000000000000..10a987e147f1
--- /dev/null
+++ b/src/dev-app/cdk-experimental-nav/cdk-nav-demo.html
@@ -0,0 +1,4 @@
+
+
Listbox using UI Patterns
+
+
diff --git a/src/dev-app/cdk-experimental-nav/cdk-nav-demo.ts b/src/dev-app/cdk-experimental-nav/cdk-nav-demo.ts
new file mode 100644
index 000000000000..1ea174f2be74
--- /dev/null
+++ b/src/dev-app/cdk-experimental-nav/cdk-nav-demo.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {ChangeDetectionStrategy, Component} from '@angular/core';
+import {CdkNavExample} from '@angular/components-examples/cdk-experimental/nav';
+
+@Component({
+ templateUrl: 'cdk-nav-demo.html',
+ imports: [CdkNavExample],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CdkExperimentalNavDemo {}
diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts
index 12a9f56de8f0..0c8c9dfc7cb0 100644
--- a/src/dev-app/dev-app/dev-app-layout.ts
+++ b/src/dev-app/dev-app/dev-app-layout.ts
@@ -61,6 +61,7 @@ export class DevAppLayout {
{name: 'CDK Dialog', route: '/cdk-dialog'},
{name: 'CDK Experimental Combobox', route: '/cdk-experimental-combobox'},
{name: 'CDK Experimental Listbox', route: '/cdk-experimental-listbox'},
+ {name: 'CDK Experimental Nav', route: '/cdk-experimental-nav'},
{name: 'CDK Experimental Tabs', route: '/cdk-experimental-tabs'},
{name: 'CDK Listbox', route: '/cdk-listbox'},
{name: 'CDK Menu', route: '/cdk-menu'},
diff --git a/src/dev-app/routes.ts b/src/dev-app/routes.ts
index bc01db461312..3f8ffea231b4 100644
--- a/src/dev-app/routes.ts
+++ b/src/dev-app/routes.ts
@@ -50,6 +50,11 @@ export const DEV_APP_ROUTES: Routes = [
loadComponent: () =>
import('./cdk-experimental-listbox/cdk-listbox-demo').then(m => m.CdkExperimentalListboxDemo),
},
+ {
+ path: 'cdk-experimental-nav',
+ loadComponent: () =>
+ import('./cdk-experimental-nav/cdk-nav-demo').then(m => m.CdkExperimentalNavDemo),
+ },
{
path: 'cdk-experimental-tabs',
loadComponent: () =>