Oder das Prioritätsnavigationsmuster, oder ein progressiv kollabierendes Navigationsmenü. Wir können es auf mindestens drei Arten benennen.
Es gibt mehrere UX-Lösungen für Tabs und Menüs, und jede von ihnen hat ihre eigenen Vorteile gegenüber einer anderen. Sie müssen nur die beste für den Fall auswählen, den Sie lösen möchten. In der Design- und Entwicklungsagentur Kollegorna debattierten wir über die am besten geeignete UX-Technik für Tabs für die Website unseres Kunden…
Wir einigten uns darauf, dass es eine One-Liner sein sollte, da die Anzahl der Tab-Elemente unbekannt ist, und grenzten unsere Optionen auf zwei ein: horizontales Scrollen und adaptive Anpassung mit einem "Mehr"-Button. Erstens ist das Problem bei ersterem, dass horizontales Scrollen als Funktion für Benutzer nicht immer visuell offensichtlich ist (insbesondere bei schmalen Elementen wie Tabs), während was sonst offensichtlicher sein kann als ein Button ("Mehr"), richtig? Zweitens ist das horizontale Scrollen mit einem mausgesteuerten Gerät keine sehr bequeme Sache, so dass wir möglicherweise unsere UI mit zusätzlichen Pfeiltasten komplexer gestalten müssen. Alles in allem entschieden wir uns für letztere Option

Planung
Die Hauptintrige hier ist, ob es möglich ist, das ohne JavaScript? zu erreichen. Teilweise ja, aber die Einschränkungen, die damit einhergehen, machen es wahrscheinlich nur für ein Museumskonzept gut und nicht für reale Szenarien (jedenfalls hat Kenan wirklich gute Arbeit geleistet). Trotzdem bedeutet die Abhängigkeit von JS nicht, dass wir es nicht nutzbar machen können, falls die Technologie aus irgendeinem Grund nicht verfügbar ist. Progressive Enhancement und Graceful Degradation sind die Gewinner!
Da die Anzahl der Tab-Elemente unsicher oder volatil ist, werden wir Flexbox verwenden, die sicherstellt, dass die Elemente schön im Container-Element verteilt sind, ohne die Breiten festzulegen.
Erstes Prototyp
Es gibt zwei Listen, sowohl visuell als auch technisch: eine für Elemente, die in den Container passen, und eine für Elemente, die nicht passen. Da wir uns auf JavaScript verlassen werden, ist es völlig in Ordnung, unseren anfänglichen Markup mit nur einer Liste zu haben (wir werden sie mit JS duplizieren)
<nav class="tabs">
<ul class="-primary">
<li><a href="...">Falkenberg</a></li>
<li><a href="...">Braga</a></li>
<!-- ... -->
</ul>
</nav>
Mit einer winzigen Prise Flexbox-basiertem CSS werden die Dinge hier ernst. Ich werde die dekorativen CSS-Eigenschaften in meinen Beispielen hier überspringen und hier das platzieren, was wirklich wichtig ist
.tabs .-primary {
display: flex;
}
.tabs .-primary > li {
flex-grow: 1;
}
Hier ist, was wir bereits haben

Graceful Degradation
Bevor wir es mit JavaScript progressiv verbessern, stellen wir sicher, dass es gracefully abbaut, wenn kein JS verfügbar ist. Es gibt mehrere Gründe für die Abwesenheit von JS: es wird noch geladen, es gibt fatale Fehler, es konnte nicht über das Netzwerk übertragen werden.
.tabs:not(.--jsfied) {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
Und sobald JavaScript vorhanden ist, wird der Klasse --jsfied zum Container-Element hinzugefügt, was das obige CSS *neutralisiert*
container.classList.add('--jsfied')
Es stellt sich heraus, dass die horizontale Scroll-Strategie, die ich zuvor erwähnt habe, hier gut genutzt werden könnte! Wenn nicht genügend Platz für Menüelemente vorhanden ist, werden die überlaufenden Inhalte im Container abgeschnitten und Scrollbalken angezeigt. Das ist doch viel besser als leere Flächen oder etwas Kaputtes, nicht wahr?

Fehlende Teile
Zuerst fügen wir die fehlenden DOM-Teile ein
- Sekundäre (Dropdown-)Liste, die eine Kopie der Hauptliste ist;
- "Mehr"-Button.
const container = document.querySelector('.tabs')
const primary = container.querySelector('.-primary')
const primaryItems = container.querySelectorAll('.-primary > li:not(.-more)')
container.classList.add('--jsfied')
// insert "more" button and duplicate the list
primary.insertAdjacentHTML('beforeend', `
<li class="-more">
<button type="button" aria-haspopup="true" aria-expanded="false">
More ↓
</button>
<ul class="-secondary">
${primary.innerHTML}
</ul>
</li>
`)
const secondary = container.querySelector('.-secondary')
const secondaryItems = secondary.querySelectorAll('li')
const allItems = container.querySelectorAll('li')
const moreLi = primary.querySelector('.-more')
const moreBtn = moreLi.querySelector('button')
moreBtn.addEventListener('click', (e) => {
e.preventDefault()
container.classList.toggle('--show-secondary')
moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary'))
})
Hier verschachteln wir die *sekundäre* Liste in die *primäre* und verwenden einige aria-* Eigenschaften. Wir wollen, dass unser Navigationsmenü barrierefrei ist, richtig?
Es gibt auch einen Event-Handler, der an den "Mehr"-Button gebunden ist und die Klasse --show-secondary auf dem Container-Element umschaltet. Wir werden ihn verwenden, um die sekundäre Liste ein- und auszublenden. Nun stylen wir die neuen Teile. Sie möchten den "Mehr"-Button visuell hervorheben.
.tabs {
position: relative;
}
.tabs .-secondary {
display: none;
position: absolute;
top: 100%;
right: 0;
}
.tabs.--show-secondary .-secondary {
display: block;
}
Hier sind wir angekommen

Offensichtlich brauchen wir Code, der die Tabs ein- und ausblendet…
Tabs in den Listen ein- und ausblenden
Aufgrund von Flexbox werden die Tab-Elemente niemals in mehrere Zeilen umbrechen und auf ihre kleinstmöglichen Breiten schrumpfen. Das bedeutet, dass wir jedes Element einzeln durchlaufen, ihre Breiten aufsummieren, sie mit der Breite des .tabs-Elements vergleichen und die Sichtbarkeit bestimmter Tabs entsprechend umschalten können. Dazu erstellen wir eine Funktion namens doAdapt und packen den folgenden Code in diesen Abschnitt.
Zuerst enthüllen wir visuell alle Elemente
allItems.forEach((item) => {
item.classList.remove('--hidden')
})
Nebenbei bemerkt, .--hidden funktioniert so, wie Sie es wahrscheinlich erwartet haben
.tabs .--hidden {
display: none;
}
Mathe-Zeit! Ich muss Sie enttäuschen, wenn Sie fortgeschrittene Mathematik erwartet haben. Wie bereits beschrieben, durchlaufen wir jeden *primären* Tab, indem wir ihre Breiten unter der Variablen stopWidth summieren. Wir führen auch eine Prüfung durch, ob das Element in den Container passt, blenden das Element aus, falls nicht, und speichern seinen Index für später.
let stopWidth = moreBtn.offsetWidth
let hiddenItems = []
const primaryWidth = primary.offsetWidth
primaryItems.forEach((item, i) => {
if(primaryWidth >= stopWidth + item.offsetWidth) {
stopWidth += item.offsetWidth
} else {
item.classList.add('--hidden')
hiddenItems.push(i)
}
})
Danach müssen wir die entsprechenden Elemente aus der *sekundären* Liste ausblenden, die in der *primären* Liste sichtbar geblieben sind. Ebenso blenden wir den "Mehr"-Button aus, wenn keine Tabs ausgeblendet wurden.
if(!hiddenItems.length) {
moreLi.classList.add('--hidden')
container.classList.remove('--show-secondary')
moreBtn.setAttribute('aria-expanded', false)
}
else {
secondaryItems.forEach((item, i) => {
if(!hiddenItems.includes(i)) {
item.classList.add('--hidden')
}
})
}
Stellen Sie abschließend sicher, dass die Funktion doAdapt zu den richtigen Zeitpunkten ausgeführt wird
doAdapt() // adapt immediately on load
window.addEventListener('resize', doAdapt) // adapt on window resize
Idealerweise sollte der Resize-Event-Handler debounced werden, um unnötige Berechnungen zu vermeiden.
Meine Damen und Herren, hier ist das Ergebnis (spielen Sie mit der Größenänderung des Demo-Fensters)
Siehe den Pen Container-Adapting Tabs With “More” Button von Osvaldas (@osvaldas) auf CodePen.
Ich könnte meinen Artikel hier beenden, aber es gibt noch einen Schritt, den wir gehen können, um ihn besser zu machen, und einige Dinge zu beachten…
Verbesserungen
Es wurde in der obigen Demo implementiert, aber wir haben ein kleines Detail nicht besprochen, das die UX unseres Tab-Widgets verbessert. Es ist das automatische Ausblenden der Dropdown-Liste, wenn der Benutzer irgendwo außerhalb der Liste klickt. Dafür können wir einen globalen Klick-Listener binden und prüfen, ob das angeklickte Element oder eines seiner Eltern die *sekundäre* Liste oder der "Mehr"-Button ist. Wenn nicht, wird die Dropdown-Liste geschlossen.
document.addEventListener('click', (e) => {
let el = e.target
while(el) {
if(el === secondary || el === moreBtn) {
return;
}
el = el.parentNode
}
container.classList.remove('--show-secondary')
moreBtn.setAttribute('aria-expanded', false)
})
Randfälle
Lange Tab-Titel
Sie haben sich vielleicht gefragt, wie sich das Widget bei langen Tab-Titeln verhält. Nun, dafür haben Sie mindestens zwei Möglichkeiten…
- Lassen Sie Titel in die nächste Zeile umbrechen, wie sie sich standardmäßig verhalten (Sie können auch den Zeilenumbruch mit
word-wrap: break-wordaktivieren)
- Oder Sie können alle Arten von Umbrüchen in der *primären* Liste mit
white-space: nowrapdeaktivieren. Das Skript ist flexibel genug, um die *zu langen* Elemente in das Dropdown zu verschieben (wo die Titel frei umbrechen dürfen), indem es die kürzeren Geschwister beiseite lässt

Viele Tabs
Auch wenn die *sekundäre* Liste position: absolute hat, spielt es keine Rolle, wie lang die Höhe Ihres Dokuments ist. Solange das Container-Element oder seine Eltern nicht position: fixed sind, passt sich das Dokument an und die unteren Elemente sind durch Scrollen auf der Seite erreichbar.
Eine Sache, die man beachten sollte
Die Dinge können knifflig werden, wenn die Tabs Buttons und keine semantischen Anker sind, was bedeutet, dass ihre Reaktion auf Klicks von JavaScript bestimmt wird, z. B.: dynamische Tabs. Das Problem hier ist, dass Tab-Button-Event-Handler nicht zusammen mit dem Markup dupliziert werden. Ich sehe mindestens zwei Ansätze, um das zu lösen
- Platzieren Sie dynamische Event-Handler-Anbindungen direkt nach dem adaptiven Tab-Code;
- Verwenden Sie stattdessen eine Event-Delegationsmethode (denken Sie an
live()von jQuery).
Leider treten Events in Mengen auf: Wahrscheinlich haben Ihre Tabs einen *ausgewählten* Zustand, der den aktuellen Tab visuell anzeigt, daher ist es auch wichtig, die Zustände gleichzeitig zu verwalten. Andernfalls schalten Sie das Tablet um und Sie sind verloren.
Browserkompatibilität
Auch wenn ich in den Beispielen und der Demo ES6-Syntax verwendet habe, sollte sie von einem Compiler wie Babel in ES5 konvertiert werden, um die Browserunterstützung erheblich zu erweitern (bis einschließlich IE9).
Sie können die Flexbox-Implementierung auch mit einer älteren Version und Syntax (bis IE10) erweitern. Wenn Sie auch nicht-Flexbox-Browser unterstützen müssen, können Sie immer Feature-Erkennung mit CSS @supports durchführen, die Technik progressiv anwenden und sich für ältere Browser auf horizontales Scrollen verlassen.
Viel Spaß beim Tabben!
Vor einiger Zeit versuchte ich, eine Priority-Navigation nur mit CSS zu realisieren. Es funktioniert, sollte aber eher als Proof of Concept betrachtet werden. Unterstützung für Tastaturnavigation und Barrierefreiheit sollte hinzugefügt werden, wenn es in einer realen Umgebung verwendet wird. Aber trotzdem ein lustiges kleines Experiment.
Ich mag den Fade-Out, der leicht andeutet, dass mehr Menüeinträge verfügbar sind.
Das gefällt mir sehr gut! Clevere Nutzung von absoluter Positionierung für den "Menü"-Link, und ich schätze den dezenten Fade-Out des Elements, das an den "Menü"-Link grenzt.
@olachristensson, das ist ein großartiges Beispiel. ;)
Es gibt eine weitere reine CSS-Workaround-Lösung von Roman http://kizu.ru/fun/chevron/ (HTML-Semantik ist schlecht, aber ein toller Eindruck, die CSS-Fähigkeit zu sehen).
Ich erinnere mich, dass vor ein paar Jahren die Verwendung eines doppelten Bindestrichs "–" am Anfang eines Klassennamens von einigen, wenn nicht allen Browsern nicht akzeptiert wurde. Es sei denn, das hat sich kürzlich geändert, ich glaube nicht, dass ".–jsfied" funktionieren wird?
Ah, es sieht so aus, als ob sich das geändert hat
https://stackoverflow.com/questions/30819462/can-css-identifiers-begin-with-two-hyphens#answer-30822662
Machen Sie weiter.
Ein –Klassenname ist erlaubt (Doppelbindestrich), aber was ist mit dem -primary (Einzelbindestrich)-Klassennamen, der im allerersten HTML-Block verwendet wird? Soweit ich weiß, ist das nicht erlaubt.
Ich hatte eine reine CSS-Implementierung, bei der versteckte Radio-Buttons und eine Checkbox verwendet wurden. Ich werde wahrscheinlich dafür geröstet, dass ich DL, DT und DD-Tags für Tabs und Tab-Panels verwendet habe, aber egal.
Kleine Kleinigkeit, die
live()-Methode von jQuery ist seit langem veraltet. Sie möchten vielleicht dieon()-Methode vorschlagen. Siehe: https://api.jquery.com/live/Gute Arbeit! Wie würden Sie mit der Situation umgehen, in der Sie die strikte Reihenfolge benötigen? Z. B. Sie möchten nicht, dass das letzte Element vor dem vorherigen erscheint (z. B. im Website-Menü möchten Sie vielleicht "Kontakt uns" nicht vor dem "Mehr"-Button anzeigen).
Ist das eine einfache Lösung? Ich gehe davon aus, dass die Breite des "Mehr"-Buttons berücksichtigt werden muss.