Componentes CSS de navegaciones y menús

Navegación horizontal con indicadores de desbordamiento

Se trata de una navegación sencilla. Cuando la navegación no tiene suficiente espacio (en móviles, por ejemplo) los indicadores de desbordamiento con gradiente blanco (a la derecha y a la izquierda) se muestran para señalar que el menú es desplazable. En caso de que no quieras estos indicadores, puedes eliminarlos. No se olvide de eliminar el javascript y CSS para ellos también. Si hay algún elemento activo en el menú, se desplaza automáticamente a la vista.
<!--
stylify-variables
	color: '#FFA585'
/stylify-variables
-->
<nav class="
	js-nav
	position:relative overflow:hidden
	[.nav-overflow-indicator]{width:32px;transition:.3s;will-change:transform;height:100%;display:flex;position:absolute;top:0}
">
	<div class="
		s-hidden
		js-nav-overflow-indicator nav-overflow-indicator
		left:0 background:linear-gradient(90deg,#fff,rgba(255,255,255,0))
		[&.s-hidden]{transform:translateX(-50px)}
	"></div>
	<div class="
		js-nav-scrollbar
		display:flex gap:16px overflow:auto
		[a]{text-decoration:none;color:#000;font-weight:bold;white-space:nowrap}
		[a:hover,a.s-selected]{color:$color}
	">
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#" class="s-selected">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
	</div>
	<div class="
		s-hidden
		js-nav-overflow-indicator nav-overflow-indicator
		right:0 background:linear-gradient(-90deg,#fff,rgba(255,255,255,0))
		[&.s-hidden]{transform:translateX(50px)}
	"></div>
</nav>

<script>
	const scrollLinkIntoView = (link) => {
		// TODO this is disabled because it cause scroll on Stylify web
		// Uncomment this so the selected item scrolls into the view
		//link.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
	};

	const getNavScrollBar = (nav) => nav.querySelector('.js-nav-scrollbar');
	const getNavScrollIndicators = (nav) => {
		const indicators = nav.querySelectorAll('.js-nav-overflow-indicator');

		return {
			left: indicators[0],
			right: indicators[1]
		}
	}

	const indicatorHiddenClass = 's-hidden';
	const scrollIndicatorLimitToHide = 24;
	const toggleNavScrollIndicators = (nav, scrollBar) => {
		scrollBar = scrollBar ?? getNavScrollBar(nav);
		const navWidth = parseInt(window.getComputedStyle(nav).width.match(/\d+/));
		const scrollBarScrollWidth = scrollBar.scrollWidth;
		const indicators = getNavScrollIndicators(nav);

		if (scrollBarScrollWidth <= navWidth) {
			indicators.left.classList.add(indicatorHiddenClass);
			indicators.right.classList.add(indicatorHiddenClass);
			return;
		}

		const leftOverflow = scrollBar.scrollLeft;

		indicators.left.classList.toggle(indicatorHiddenClass, leftOverflow <= scrollIndicatorLimitToHide);

		indicators.right.classList.toggle(
			indicatorHiddenClass,
			scrollBarScrollWidth - leftOverflow <= navWidth + scrollIndicatorLimitToHide
		);
	}

	const navigations = document.querySelectorAll('.js-nav');

	navigations.forEach((nav) => {
		const scrollBar = getNavScrollBar(nav);
		const indicators = getNavScrollIndicators(nav);

		nav.querySelectorAll('a').forEach((link) => link.addEventListener('click', () => scrollLinkIntoView(link)));

		for (const indicator of Object.values(indicators)) {
			indicator.addEventListener('touchstart', () => indicator.classList.add(indicatorHiddenClass));
			indicator.addEventListener('mouseover', () => indicator.classList.add(indicatorHiddenClass));
			indicator.addEventListener('mouseout', () => toggleNavScrollIndicators(nav, scrollBar));
		}

		scrollBar.addEventListener('scroll', () => toggleNavScrollIndicators(nav, scrollBar));

		toggleNavScrollIndicators(nav);
	});

	window.addEventListener('load', () => {
		document.querySelectorAll('.s-selected').forEach((link) => scrollLinkIntoView(link));
	});

	window.addEventListener('resize', () => navigations.forEach((nav) => toggleNavScrollIndicators(nav)));
</script>
<!--
stylify-variables
	color: '#FFA585'
/stylify-variables

stylify-components
	navigation: `
		position:relative overflow:hidden

		.nav-overflow-indicator {
			width:32px transition:.3s will-change:transform height:100% display:flex position:absolute top:0
		}

		.nav-overflow-indicator--left {
			left:0 background:linear-gradient(90deg,#fff,rgba(255,255,255,0))
			&.s-hidden {transform:translateX(-50px)}
		}

		.nav-overflow-indicator--right {
			right:0 background:linear-gradient(-90deg,#fff,rgba(255,255,255,0))
			&.s-hidden { transform:translateX(50px) }
		}

		.nav-scrollbar {
			display:flex gap:16px overflow:auto
			a {
				text-decoration:none color:#000 font-weight:bold white-space:nowrap
				&:hover, &.s-selected { color:$color }
			}
		}
	`
/stylify-components
-->
<nav class="js-nav navigation position:relative overflow:hidden">
	<div class="s-hidden js-nav-overflow-indicator nav-overflow-indicator nav-overflow-indicator--left"></div>
	<div class="js-nav-scrollbar nav-scrollbar">
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#" class="s-selected">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
		<a href="#">Link</a>
	</div>
	<div class="s-hidden js-nav-overflow-indicator nav-overflow-indicator nav-overflow-indicator--right"></div>
</nav>

<script>
	const scrollLinkIntoView = (link) => {
		link.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
	};

	const getNavScrollBar = (nav) => nav.querySelector('.js-nav-scrollbar');
	const getNavScrollIndicators = (nav) => {
		const indicators = nav.querySelectorAll('.js-nav-overflow-indicator');

		return {
			left: indicators[0],
			right: indicators[1]
		}
	}

	const indicatorHiddenClass = 's-hidden';
	const scrollIndicatorLimitToHide = 24;
	const toggleNavScrollIndicators = (nav, scrollBar) => {
		scrollBar = scrollBar ?? getNavScrollBar(nav);
		const navWidth = parseInt(window.getComputedStyle(nav).width.match(/\d+/));
		const scrollBarScrollWidth = scrollBar.scrollWidth;
		const indicators = getNavScrollIndicators(nav);

		if (scrollBarScrollWidth <= navWidth) {
			indicators.left.classList.add(indicatorHiddenClass);
			indicators.right.classList.add(indicatorHiddenClass);
			return;
		}

		const leftOverflow = scrollBar.scrollLeft;

		indicators.left.classList.toggle(indicatorHiddenClass, leftOverflow <= scrollIndicatorLimitToHide);

		indicators.right.classList.toggle(
			indicatorHiddenClass,
			scrollBarScrollWidth - leftOverflow <= navWidth + scrollIndicatorLimitToHide
		);
	}

	const navigations = document.querySelectorAll('.js-nav');

	navigations.forEach((nav) => {
		const scrollBar = getNavScrollBar(nav);
		const indicators = getNavScrollIndicators(nav);

		nav.querySelectorAll('a').forEach((link) => link.addEventListener('click', () => scrollLinkIntoView(link)));

		for (const indicator of Object.values(indicators)) {
			indicator.addEventListener('touchstart', () => indicator.classList.add(indicatorHiddenClass));
			indicator.addEventListener('mouseover', () => indicator.classList.add(indicatorHiddenClass));
			indicator.addEventListener('mouseout', () => toggleNavScrollIndicators(nav, scrollBar));
		}

		scrollBar.addEventListener('scroll', () => toggleNavScrollIndicators(nav, scrollBar));

		toggleNavScrollIndicators(nav);
	});

	window.addEventListener('load', () => {
		document.querySelectorAll('.s-selected').forEach((link) => scrollLinkIntoView(link));
	});

	window.addEventListener('resize', () => navigations.forEach((nav) => toggleNavScrollIndicators(nav)));
</script>
Menú móvil simple que se abre tras hacer clic en un icono de hamburguesa.
<!--
stylify-variables
	color: '#C81D77'
/stylify-variables
-->

Click to open the navigation
<a role="button" class="
	js-mobile-nav-toggle
	display:inline-flex align-items:center justify-content:center width:42px height:42px border-radius:50px cursor:pointer background:$color transition:background_.3s
	hover:background:lighten($color,20)
	focus:background:lighten($color,20)
">
	<svg viewBox="0 0 100 80" fill="#fff" width="16" height="16">
		<rect width="100" height="20" rx="10"></rect>
		<rect y="30" width="100" height="20" rx="10"></rect>
		<rect y="60" width="100" height="20" rx="10"></rect>
	</svg>
</a>

<aside class="
	js-mobile-nav s-hidden
	will-change:transform
	transition:transform_.3s display:flex align-items:flex-end position:fixed top:0 right:0 flex-direction:column width:100vw height:100vh z-index:2
	[&.s-hidden]{transform:translateX(100%)}
">
	<div class="js-mobile-nav-obfuscator position:absolute inset:0 background:rgba(0,0,0,.5)"></div>
	<nav role="navigation" class="
		display:flex flex-direction:column position:relative
		background:#fff height:100vh width:70% max-width:300px
	">
		<div class="position:absolute width:100% border-bottom:1px_solid_#eee z-index:2 top:0 left:0 padding:12px display:flex justify-content:space-between align-items:center background:rgba(255,255,255,0.8) backdrop-filter:blur(3px)">
			<a href="#" aria-label="Home Button" class="display:flex">
				<img src="https://stylifycss.com/images/logo/horizontal.svg" loading="lazy" fetchpriority="low" width="100" height="28">
			</a>
			<a role="button" aria-label="Close Menu" class="cursor:pointer align-self:flex-end js-mobile-nav-toggle">
				<svg width="40" height="40">
					<path d="m10 10 20 20m0-20L10 30" stroke="#000" stroke-width="4"/>
				</svg>
			</a>
		</div>
		<div class="
			gap:5% display:flex justify-content:flex-end
			[a]{word-break:break-word;font-weight:bold;white-space:normal;text-decoration:none;color:#000;padding:8px_0;hover:color:$color}
			[a:not(:last-of-type)]{border-bottom:1px_solid_#eee}
			[ul]{list-style:none}
			[li::before]{color:orange}
			flex-direction:column gap:12px height:100vh padding:84px_12px_12px_12px overflow:auto justify-content:flex-start
		">
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
		</div>
	</nav>
</aside>

<script>
	const mobileNavHiddenClass = 's-hidden';
	const mobileNavClassPrefix = 'js-mobile-nav';

	const toggleMobileNavigationVisibility = () => {
		const headerNavigation = document.querySelector(`.${mobileNavClassPrefix}`);

		if (headerNavigation?.classList.contains(mobileNavHiddenClass)) {
			headerNavigation.classList.remove(mobileNavHiddenClass);
		} else {
			headerNavigation?.classList.add(mobileNavHiddenClass);
		}
	}

	document.querySelector(`.${mobileNavClassPrefix}-obfuscator`).addEventListener('click', () => toggleMobileNavigationVisibility());

	document.querySelectorAll(`.${mobileNavClassPrefix}-toggle`).forEach((button) => {
		button.addEventListener('click', () => toggleMobileNavigationVisibility());
	});

</script>
<!--
stylify-variables
	color: '#C81D77'
/stylify-variables

stylify-components
	'mobile-navigation': `
		will-change:transform
		transition:transform_.3s display:flex align-items:flex-end position:fixed top:0 right:0 flex-direction:column width:100vw height:100vh z-index:2
		&.s-hidden { transform:translateX(100%) }
	`,
	'mobile-navigation__header': `
		position:absolute width:100% border-bottom:1px_solid_#eee z-index:2 top:0 left:0 padding:12px display:flex justify-content:space-between align-items:center background:rgba(255,255,255,0.8) backdrop-filter:blur(3px)
	`,
	'mobile-navigation__header-close': 'cursor:pointer align-self:flex-end',
	'mobile-navigation__links': `
		gap:5% display:flex justify-content:flex-end flex-direction:column gap:12px height:100vh padding:84px_12px_12px_12px overflow:auto justify-content:flex-start
		a { word-break:break-word font-weight:bold white-space:normal text-decoration:none color:#000 padding:8px_0 hover:color:$color }
		a:not(:last-of-type) { border-bottom:1px_solid_#eee }
		ul { list-style:none }
		li::before { color:orange }
	`,
	'mobile-navigation-hamburger': `
		display:inline-flex align-items:center justify-content:center width:42px height:42px border-radius:50px cursor:pointer background:$color transition:background_.3s
		hover:background:lighten($color,20)
		focus:background:lighten($color,20)
	`
/stylify-components
-->

Click to open the navigation
<a role="button" class="js-mobile-nav-toggle mobile-navigation-hamburger">
	<svg viewBox="0 0 100 80" fill="#fff" width="16" height="16">
		<rect width="100" height="20" rx="10"></rect>
		<rect y="30" width="100" height="20" rx="10"></rect>
		<rect y="60" width="100" height="20" rx="10"></rect>
	</svg>
</a>

<aside class="js-mobile-nav s-hidden mobile-navigation">
	<div class="js-mobile-nav-obfuscator position:absolute inset:0 background:rgba(0,0,0,.5)"></div>
	<nav role="navigation" class="
		display:flex flex-direction:column position:relative
		background:#fff height:100vh width:70% max-width:300px
	">
		<div class="mobile-navigation__header">
			<a href="#" aria-label="Home Button" class="display:flex">
				<img src="https://stylifycss.com/images/logo/horizontal.svg" loading="lazy" fetchpriority="low" width="100" height="28">
			</a>
			<a role="button" aria-label="Close Menu" class="mobile-navigation__header-close js-mobile-nav-toggle">
				<svg width="40" height="40">
					<path d="m10 10 20 20m0-20L10 30" stroke="#000" stroke-width="4"/>
				</svg>
			</a>
		</div>
		<div class="mobile-navigation__links">
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
			<a href="#" class="link">Menu link</a>
		</div>
	</nav>
</aside>

<script>
	const mobileNavHiddenClass = 's-hidden';
	const mobileNavClassPrefix = 'js-mobile-nav';

	const toggleMobileNavigationVisibility = () => {
		const headerNavigation = document.querySelector(`.${mobileNavClassPrefix}`);

		if (headerNavigation?.classList.contains(mobileNavHiddenClass)) {
			headerNavigation.classList.remove(mobileNavHiddenClass);
		} else {
			headerNavigation?.classList.add(mobileNavHiddenClass);
		}
	}

	document.querySelector(`.${mobileNavClassPrefix}-obfuscator`).addEventListener('click', () => toggleMobileNavigationVisibility());

	document.querySelectorAll(`.${mobileNavClassPrefix}-toggle`).forEach((button) => {
		button.addEventListener('click', () => toggleMobileNavigationVisibility());
	});

</script>