diff --git a/core/lib/services/index.ts b/core/lib/services/index.ts
index 842f8f67cc..849e146f4e 100644
--- a/core/lib/services/index.ts
+++ b/core/lib/services/index.ts
@@ -5,3 +5,4 @@ export * from './intersection';
 export * from './portal';
 export * from './stores';
 export * from './writables';
+export * from './navManager';
diff --git a/core/lib/services/navManager.spec.ts b/core/lib/services/navManager.spec.ts
new file mode 100644
index 0000000000..eaa982549f
--- /dev/null
+++ b/core/lib/services/navManager.spec.ts
@@ -0,0 +1,67 @@
+import {beforeEach, describe, expect, test} from 'vitest';
+import {createNavManager} from './navManager';
+
+describe(`navManager`, () => {
+	let parentElement: HTMLElement;
+	beforeEach(() => {
+		parentElement = document.body.appendChild(document.createElement('div'));
+		return () => {
+			parentElement.parentElement?.removeChild(parentElement);
+		};
+	});
+	const sendKey = (key: 'ArrowLeft' | 'ArrowRight') => document.activeElement!.dispatchEvent(new KeyboardEvent('keydown', {key}));
+
+	test('Basic functionalities', () => {
+		parentElement.innerHTML = `
+			<span id="element1"></span>
+			<input type="checkbox" id="element2">
+			<input type="text" id="element3" value="some content">
+			<span id="element4"></span>
+		`;
+		const element1 = document.getElementById('element1')!;
+		const element2 = document.getElementById('element2')!;
+		const element3 = document.getElementById('element3') as HTMLInputElement;
+		const element4 = document.getElementById('element4')!;
+		const navManager = createNavManager();
+		const directive1 = navManager.directive(element1);
+		const directive2 = navManager.directive(element2);
+		// intentionnally not called in DOM order to check that the array is properly sorted:
+		const directive4 = navManager.directive(element4);
+		const directive3 = navManager.directive(element3);
+		element1.focus();
+		expect(document.activeElement).toBe(element1);
+		sendKey('ArrowRight');
+		expect(document.activeElement).toBe(element2);
+		sendKey('ArrowRight');
+		expect(document.activeElement).toBe(element3);
+		element3.setSelectionRange(0, 0);
+		sendKey('ArrowRight');
+		// as the cursor is not at the end yet, the focus did not move
+		expect(document.activeElement).toBe(element3);
+		element3.setSelectionRange(element3.value.length, element3.value.length);
+		sendKey('ArrowRight');
+		expect(document.activeElement).toBe(element4);
+		sendKey('ArrowRight');
+		// last element, the focus cannot move:
+		expect(document.activeElement).toBe(element4);
+		// now go backward:
+		sendKey('ArrowLeft');
+		expect(document.activeElement).toBe(element3);
+		element3.setSelectionRange(1, 1);
+		sendKey('ArrowLeft');
+		// as the cursor is not at the beginning yet, the focus did not move
+		expect(document.activeElement).toBe(element3);
+		element3.setSelectionRange(0, 0);
+		sendKey('ArrowLeft');
+		expect(document.activeElement).toBe(element2);
+		sendKey('ArrowLeft');
+		expect(document.activeElement).toBe(element1);
+		sendKey('ArrowLeft');
+		// first element, the focus cannot move:
+		expect(document.activeElement).toBe(element1);
+		directive1?.destroy?.();
+		directive2?.destroy?.();
+		directive3?.destroy?.();
+		directive4?.destroy?.();
+	});
+});
diff --git a/core/lib/services/navManager.ts b/core/lib/services/navManager.ts
new file mode 100644
index 0000000000..5e1d12040f
--- /dev/null
+++ b/core/lib/services/navManager.ts
@@ -0,0 +1,58 @@
+import type {Directive} from '../types';
+import {compareDomOrder} from './sortUtils';
+import {registrationArray} from './directiveUtils';
+import {computed} from '@amadeus-it-group/tansu';
+
+export type NavManager = ReturnType<typeof createNavManager>;
+
+// cf https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
+const textInputTypes = new Set(['text', 'search', 'url', 'tel', 'password']);
+const isTextInput = (element: any): element is HTMLInputElement => element instanceof HTMLInputElement && textInputTypes.has(element.type);
+
+export const createNavManager = () => {
+	const array$ = registrationArray<HTMLElement>();
+	const sortedArray$ = computed(() => [...array$()].sort(compareDomOrder));
+	const directive: Directive = (element) => {
+		const onKeyDown = (event: KeyboardEvent) => {
+			let move = 0;
+			switch (event.key) {
+				case 'ArrowLeft':
+					move = -1;
+					break;
+				case 'ArrowRight':
+					move = 1;
+					break;
+			}
+			if (isTextInput(event.target)) {
+				const cursorPosition = event.target.selectionStart === event.target.selectionEnd ? event.target.selectionStart : null;
+				if ((cursorPosition !== 0 && move < 0) || (cursorPosition !== event.target.value.length && move > 0)) {
+					move = 0;
+				}
+			}
+			if (move != 0) {
+				const array = sortedArray$();
+				const currentIndex = array.indexOf(element);
+				const newIndex = currentIndex + move;
+				if (newIndex < array.length && newIndex >= 0) {
+					const newItem = array[newIndex];
+					event.preventDefault();
+					newItem.focus();
+					if (isTextInput(newItem)) {
+						const position = move < 0 ? newItem.value.length : 0;
+						newItem.setSelectionRange(position, position);
+					}
+				}
+			}
+		};
+		element.addEventListener('keydown', onKeyDown);
+		const unregister = array$.register(element);
+		return {
+			destroy() {
+				element.removeEventListener('keydown', onKeyDown);
+				unregister();
+			},
+		};
+	};
+
+	return {directive};
+};
diff --git a/core/lib/services/sortUtils.spec.ts b/core/lib/services/sortUtils.spec.ts
new file mode 100644
index 0000000000..ac3f97a038
--- /dev/null
+++ b/core/lib/services/sortUtils.spec.ts
@@ -0,0 +1,26 @@
+import {describe, expect, it} from 'vitest';
+import {compareDomOrder} from './sortUtils';
+
+describe('arrayUtils', () => {
+	describe('compareDomOrder', () => {
+		it('should sort in the right order', () => {
+			const element = document.createElement('div');
+			const element1 = document.createElement('div');
+			element1.id = 'id1';
+			const element2 = document.createElement('div');
+			element2.id = 'id2';
+			const element3 = document.createElement('div');
+			element3.id = 'id3';
+			element.appendChild(element1);
+			element.appendChild(element2);
+			element.appendChild(element3);
+			expect([element1, element2, element3].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element1, element3, element2].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element2, element1, element3].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element2, element3, element1].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element3, element1, element2].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element3, element2, element1].sort(compareDomOrder)).toStrictEqual([element1, element2, element3]);
+			expect([element1, element3, element1].sort(compareDomOrder)).toStrictEqual([element1, element1, element3]);
+		});
+	});
+});
diff --git a/core/lib/services/sortUtils.ts b/core/lib/services/sortUtils.ts
new file mode 100644
index 0000000000..ab6d4b870e
--- /dev/null
+++ b/core/lib/services/sortUtils.ts
@@ -0,0 +1,14 @@
+export const compareDefault = (a: any, b: any) => (a < b ? -1 : a > b ? 1 : 0);
+
+export const compareDomOrder = (element1: Node, element2: Node) => {
+	if (element1 === element2) {
+		return 0;
+	}
+	const result = element1.compareDocumentPosition(element2);
+	if (result & Node.DOCUMENT_POSITION_FOLLOWING) {
+		return -1;
+	} else if (result & Node.DOCUMENT_POSITION_PRECEDING) {
+		return 1;
+	}
+	throw new Error('failed to compare elements');
+};