Breadcrumb

Questo pattern mostra come creare un componente breadcrumb adattabile e accessibile per consentire agli utenti di navigare nel tuo sito.

Articolo completo · Video su YouTube · Fonte su GitHub

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

       
.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;
 
}
}
       

       
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
}