或者优先级导航模式,或者逐步折叠的导航菜单。我们至少可以用三种方式命名它。
对于选项卡和菜单,有多种 UX 解决方案,它们各自都具有其他解决方案无法比拟的优势,您只需为要解决的案例选择最佳解决方案即可。在设计与开发机构 Kollegorna,我们正在就最适合客户网站选项卡的 UX 技术进行辩论……
我们一致认为它应该是 单行,因为选项卡项的数量未知,并且将我们的选择范围缩小到两种:水平滚动和带有“更多”按钮的自适应。首先,前者的问题在于,水平滚动作为一个功能并不总是对用户直观(尤其是对于选项卡等狭窄元素),而还有什么比按钮(“更多”)更直观呢?其次,使用鼠标控制设备进行水平滚动并不是一件很舒服的事情,因此我们可能需要使用额外的箭头按钮使我们的 UI 更复杂。综合考虑,我们最终选择了后一种选择

规划
这里的主要问题是是否可以在没有 JavaScript 的情况下实现?部分可以,但是它附带的限制可能使其仅适合概念博物馆,而不是现实生活中的场景(无论如何,Kenan 做得非常好)。尽管如此,对 JS 的依赖并不意味着如果出于某种原因该技术不可用,我们就无法使其可用。渐进增强和优雅降级才能获胜!
由于选项卡项的数量不确定或不稳定,我们将使用 Flexbox,它可以确保项在容器元素中均匀分布,而无需设置宽度。
初始原型
在视觉上和技术上都有两个列表:一个用于适合容器的项,另一个用于不适合的项。由于我们将依赖 JavaScript,因此使用单个列表作为初始标记是完全可以的(我们将使用 JS 复制它)
<nav class="tabs">
<ul class="-primary">
<li><a href="...">Falkenberg</a></li>
<li><a href="...">Braga</a></li>
<!-- ... -->
</ul>
</nav>
通过一点基于 flex 的 CSS,事情开始变得严肃起来。我将在这里跳过示例中的装饰性 CSS 属性,只放置真正重要的内容
.tabs .-primary {
display: flex;
}
.tabs .-primary > li {
flex-grow: 1;
}
这是我们已经拥有的

优雅降级
现在,在用 JavaScript 渐进地增强它之前,让我们确保如果没有 JS 可用,它会优雅地降级。JS 缺失的原因有很多:它仍在加载,它存在致命错误,它未能通过网络传输。
.tabs:not(.--jsfied) {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
一旦 JavaScript 出现,--jsfied
类名就会添加到容器元素中,该类名会抵消上面的 CSS
container.classList.add('--jsfied')
事实证明,我之前提到的水平滚动策略在这里可能会派上用场!当没有足够的菜单项空间时,溢出内容会在容器内被裁剪,并且会显示滚动条。这比空空间或损坏的东西好得多,不是吗?

缺失部分
首先,让我们插入缺失的 DOM 部分
- 次级(下拉)列表,它是主列表的副本;
- “更多”按钮。
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'))
})
在这里,我们将次级列表嵌套到主列表中,并使用一些aria-*
属性。我们希望我们的导航菜单是可访问的,对吧?
还有一个附加到“更多”按钮上的事件处理程序,它会切换容器元素上的--show-secondary
类名。我们将使用它来显示和隐藏次级列表。现在让我们对新部分进行样式设置。您可能希望在视觉上突出显示“更多”按钮。
.tabs {
position: relative;
}
.tabs .-secondary {
display: none;
position: absolute;
top: 100%;
right: 0;
}
.tabs.--show-secondary .-secondary {
display: block;
}
这是我们所处的状态

显然,我们需要一些隐藏和显示选项卡的代码……
隐藏和显示列表中的选项卡
由于 Flexbox,选项卡项永远不会换行,并且会缩小到其可能的最小宽度。这意味着我们可以逐个遍历每个项,将它们的宽度加起来,将其与.tabs
元素的宽度进行比较,并相应地切换特定选项卡的可见性。为此,我们将创建一个名为doAdapt
的函数,并将本节中下面的代码包装到该函数中。
首先,我们应该在视觉上显示所有项
allItems.forEach((item) => {
item.classList.remove('--hidden')
})
顺便说一下,.--hidden
按您预期的方式工作
.tabs .--hidden {
display: none;
}
数学时间!如果您期待一些高级数学,我将不得不让您失望。因此,如前所述,我们通过在stopWidth
变量下将每个主选项卡的宽度加起来来遍历每个主选项卡。我们还会检查该项是否适合容器,如果不适合,则隐藏该项,并将它的索引保存以备后用。
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)
}
})
此后,我们需要隐藏次级列表中与主列表中仍可见的项等效的项。以及在没有隐藏任何选项卡的情况下隐藏“更多”按钮。
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')
}
})
}
最后,确保doAdapt
函数在正确的时间执行
doAdapt() // adapt immediately on load
window.addEventListener('resize', doAdapt) // adapt on window resize
理想情况下,调整大小事件处理程序应该防抖,以防止不必要的计算。
女士们,先生们,这是结果(玩玩调整演示窗口的大小)
查看 CodePen 上 Osvaldas ( @osvaldas ) 的 带有“更多”按钮的容器自适应选项卡。
我可能在这里结束我的文章,但我们可以更进一步,使其变得更好,还有一些需要注意的事项……
增强功能
它已在上面的演示中实现,但我们没有概述一个小细节,它可以改善我们的选项卡小部件的 UX。它会自动隐藏下拉列表,如果用户点击列表外部的任何位置。为此,我们可以绑定一个全局点击监听器,并检查被点击的元素或其任何父元素是否为次级列表或“更多”按钮。如果不是,则下拉列表将被关闭。
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)
})
边缘情况
长选项卡标题
您可能想知道该小部件在长选项卡标题的情况下如何表现。好吧,您至少有两个选择……
- 让标题换行,这就是它们默认的行为(您也可以使用
word-wrap: break-word
启用自动换行)
- 或者,您可以使用
white-space: nowrap
在主列表中禁用所有类型的换行。该脚本足够灵活,可以通过避开较短的兄弟节点,将过长的项放到下拉菜单中(在下拉菜单中,标题可以自由换行)

多个标签
即使辅助列表是position: absolute
,也不影响文档的高度。 只要容器元素或其父元素不是position: fixed
,文档就会适应,底部项目可以通过向下滚动页面访问。
需要注意的一件事
如果标签在语义上是按钮而不是锚点,那么事情可能会变得棘手,这意味着它们的点击响应由 JavaScript 决定,例如:动态标签。 这里的问题是标签按钮事件处理程序不会与标记一起复制。 我看到至少两种解决方法
- 在自适应标签代码之后放置动态事件处理程序附件;
- 改用事件委托方法(想想 jQuery 的
live()
)。
不幸的是,事件数量很大:您的标签很可能有一个选中状态,它以视觉方式指示当前标签,因此同时管理这些状态也很重要。 否则,翻转平板电脑,你就迷路了。
浏览器兼容性
即使我在示例和演示中使用了 ES6 语法,也应该由编译器(例如 Babel)将其转换为 ES5,以显着扩大浏览器支持范围(包括 IE9)。
您还可以使用 较旧的版本和语法(一直到 IE10)扩展 Flexbox 实现。 如果您还需要支持非 Flexbox 浏览器,您可以始终使用 CSS @supports
进行特性检测,逐步应用该技术,并依赖于旧浏览器的水平滚动。
祝您标签愉快!
不久前,我尝试仅使用 CSS 来进行优先级导航。 它有效,但应被视为概念证明。 如果在实际环境中使用,则应添加对键盘导航和可访问性的支持。 但仍然,这是一个有趣的小实验。
我喜欢淡出效果,它很容易地暗示有更多菜单项可用。
我非常喜欢这个! 巧妙地使用绝对定位来实现“菜单”链接,我欣赏与“菜单”链接相邻的项目的微妙淡出效果。
@olachristensson,这是一个很好的例子。 ;)
这是 Roman 的另一个纯 CSS 解决方法 http://kizu.ru/fun/chevron/(HTML 语义不好,但看到 CSS 功能的印象很好)。
我记得几年前,在类名开头使用双破折号“–”在某些浏览器(如果不是全部)中不被接受。 除非最近有所改变,否则我认为“.–jsfied”不会起作用?
啊,看起来它确实改变了
https://stackoverflow.com/questions/30819462/can-css-identifiers-begin-with-two-hyphens#answer-30822662
继续。
a –class-name 是允许的(双破折号),但第一个 HTML 块中使用的 -primary(单破折号)类名呢? 我认为那是不允许的。
我有一个纯 CSS 实现,使用隐藏的单选按钮和一个复选框。 我可能会因为将 DL、DT 和 DD 标签用于标签和标签面板而被烤焦,但无所谓。
一个小小的挑剔,jQuery 的
live()
方法已经很久没有被弃用。 您可能想建议改用on()
方法。 参见:https://api.jqueryjs.cn/live/不错! 你将如何处理需要严格顺序的情况? 例如,您不希望最后一个项目出现在之前的项目之前(例如,在网站菜单中,您可能不希望在“更多”按钮之前显示“联系我们”)。
这是一个简单的修复吗? 我假设它需要处理“更多”按钮的宽度。