דפוס זה מראה איך ליצור רכיב רספונסיבי ונגיש של נתיבי ניווט למשתמשים לנווט באתר.
מאמר מלא · סרטון ב-YouTube · מקור ב-GitHub
HTML
<nav class="breadcrumbs" role="navigation"> <a href="./home/"> <span class="crumbicon"> <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"> <use href="#icon-home" /> </svg> </span> <span class="home-label">Home</span> </a> <span class="crumb-separator" aria-hidden="true">»</span> <span class="crumb"> <a aria-current="page">Page A</a> <span class="crumbicon"> <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"> <use href="#icon-dropdown-arrow" /> </svg> <select class="disguised-select" title="Navigate to another page"> <option selected>Page A</option> <option>Page B</option> <option>Page C</option> </select> </span> </span> </nav> <svg style="display: none;"> <symbol id="icon-home"> <title>A home icon</title> <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> </symbol> <symbol id="icon-dropdown-arrow"> <title>A down arrow</title> <path d="M19 9l-7 7-7-7" /> </symbol> </svg>
CSS
.breadcrumbs { --nav-gap: 2ch; display: flex; align-items: center; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity; gap: var(--nav-gap); padding: calc(var(--nav-gap) / 2); scroll-padding-inline: calc(var(--nav-gap) / 2); & > a:first-of-type:not(.crumb) { display: inline-flex; align-items: center; gap: calc(var(--nav-gap) / 4); @media (width <= 480px) { & > .home-label { display: none; }} } & a { text-underline-offset: .25em; outline-offset: 3px; /* fix Safari inaccessible dark color scheme links */ /* https://bugs.webkit.org/show_bug.cgi?id=226893 */ @media (prefers-color-scheme: dark) { @supports (-webkit-hyphens:none) { &[href] { color: hsl(240 100% 81%); }} } } & > .crumb:last-of-type { scroll-snap-align: end; } @supports (-webkit-hyphens:none) { scroll-snap-type: none; } } .crumb { display: inline-flex; align-items: center; gap: calc(var(--nav-gap) / 4); & > a { white-space: nowrap; &[aria-current="page"] { font-weight: bold; } } &.tree-changed ~ * { display: none; } } .crumb-separator { color: ButtonText; } .disguised-select { inline-size: 100%; block-size: 100%; opacity: .01; font-size: min(100%, 16px); } .crumbicon { --size: 3ch; display: grid; grid: [stack] var(--size) / [stack] var(--size); place-items: center; border-radius: 50%; --icon-shadow-size: 0px; box-shadow: inset 0 0 0 var(--icon-shadow-size) currentColor; @media (--motionOK) { & { transition: box-shadow .2s ease; }} @nest .crumb:is(:focus-within, :hover) > & { --icon-shadow-size: 1px; } @nest .crumb > &:is(:focus-within, :hover) { --icon-shadow-size: 2px; & svg { stroke-width: 2px; } } & > * { grid-area: stack; } & > svg { max-block-size: 100%; margin: calc(var(--nav-gap) / 4); stroke: currentColor; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1px; } }
JS
const crumbs = document.querySelectorAll('.breadcrumbs select') const allowedKeys = new Set(['Tab', 'Enter', ' ']) const preventedKeys = new Set(['ArrowUp', 'ArrowDown']) // watch crumbs for *full* changes, // ensures it's not a user exploring options via keyboard crumbs.forEach(nav => { let ignoreChange = false nav.addEventListener('change', e => { if (ignoreChange) return const option = e.target const choice = option.value const crumb = option.closest('.crumb') // flag crumb so adjacent siblings can be hidden crumb.classList.add('tree-changed') // update crumb text to reflect the user's choice crumb.querySelector(':scope > a').textContent = choice routePage(choice) }) nav.addEventListener('keydown', ({ key }) => { if (preventedKeys.has(key)) ignoreChange = true else if (allowedKeys.has(key)) ignoreChange = false }) }) const routePage = route => { console.info('change path to: ', route) // change entire URL (window.location) // or // use your favorite clientside framework's router }