两行 CSS 代码实现从移动设备到桌面的菜单

2025-06-09

两行 CSS 代码实现从移动设备到桌面的菜单

作为一名前端开发者,多年来我创建过无数菜单。这些菜单通常是网站中最复杂的元素,涵盖了移动端交互、桌面端过渡、项目重新排序等等。有一次,我为一个客户创建了一个超级菜单,它非常庞大,以至于用户误以为它是整个网页,因为它覆盖了整个页面。

然而,在我最近的项目中,我利用了一些最新和最强大的 CSS 功能来达到简单的目的。

让我们开始吧!


结构

对于标记,我能想到的最简单的结构是这样的,它仍然支持从移动弹出菜单到桌面菜单所需的灵活性:

<header>
  <a href="/">LOGO</a>
  <label>
    <input type="checkbox">
  </label>
  <div class="menu-flyout">
    <nav class="menu-main">
      <a href="#">...</a>
      <a href="#">...</a>
    </nav>
    <nav class="menu-cta">
      <a href="#">CTA</a>
      <a href="#">CTA</a>
    </nav>
  </div>
</header>
Enter fullscreen mode Exit fullscreen mode

我们稍后会添加更多类和其他内容,但这是基本结构。此版本无需任何 JavaScript即可运行,但存在可访问性问题,我们稍后也会研究。

主要元素<header>是一个flex容器,用于justify-content: space-between将徽标放置在左侧,将切换器放置在右侧:

移动菜单已折叠

.menu-flyout是两个导航块的容器:一个带有主菜单项,另一个带有 CTA(号召性用语)。

当可见时,弹出窗口将覆盖整个屏幕,否则将放置在屏幕外:

.menu-flyout {
  inset: 0;
  position: fixed;
  translate: -100vw 0;
}
Enter fullscreen mode Exit fullscreen mode

导航块都是设置为自定义属性的flex容器:flex-direction

flex-direction: var(--menu-flyout-dir, column);
Enter fullscreen mode Exit fullscreen mode

第二个导航块位于底部,使用justify-content: end

弹出按钮


切换器只是一个样式<input type="checkbox">。我们可以使用它来在点击时显示弹出窗口:

header:has(input:checked) .menu-flyout {
  translate: 0;
}
Enter fullscreen mode Exit fullscreen mode

弹出按钮

由于切换器包裹在标签中,我们可以使用它将其隐藏在桌面上:

@media (min-width: 768px) {
  label { display: none; }
}
Enter fullscreen mode Exit fullscreen mode

注意:不要担心,最终示例中的所有内容都会有类,我使用普通标签只是为了简化示例!


到目前为止一切顺利。当我们将屏幕调整到桌面大小时,我们得到:

桌面

好的,所以<label>带有切换器的 按照预期被隐藏了,<header>仍然是一个flex容器,弹出窗口仍然位于屏幕外,但现在是巨大的,占据了整个屏幕空间。

让我们用两行 CSS 来解决这个问题:

@media (min-width: 768px) {
  .menu-flyout {
    --menu-flyout-dir: row;
    display: contents;
  }
}
Enter fullscreen mode Exit fullscreen mode

其结果是:

桌面菜单

耶!如果我们检查一下,就会发现这两个导航块现在是主flex容器中的“直接”项,而且.menu-flyout似乎消失了:

弹性容器

那么到底发生了什么?

display: contents这相当于对一个元素说:“忘掉你自己的盒子,只显示你的子节点”。或者用正式的 MDN 术语来说:

这些元素本身不会生成特定的框。它们被其伪框及其子框所取代。

我们通过更新之前声明的自定义属性来更改两个导航块的 flex-direction --menu-flyout-dir


我们现在有一个可用的、从移动到桌面的菜单,它只使用很少的HTML 和 CSS,根本不使用 JavaScript 。

让我们看看还能做什么。我已将桌面最大宽度设置为1200px,但我想将其“拉伸”background-color到屏幕边缘。

这曾经需要在菜单周围添加一个额外的元素,但现在可以用一个非常大的元素来实现border-image

border-image: conic-gradient(
  hsl(240, 10%, 20%) 0 0)
  fill 0//100vw;
Enter fullscreen mode Exit fullscreen mode

好的...这实际上会覆盖整个屏幕。

让我们添加一个clip-path来解决这个问题:

clip-path: polygon(
  -100vw 0,
  100vw 0,
  100vw 100%,
  -100vw 100%
);
Enter fullscreen mode Exit fullscreen mode

现在,即使在非常大的桌面上,背景也会延伸到屏幕边缘(您可能需要放大才能看到它!):

大型桌面


移动修复

在移动设备上,我们可以使用它:has来检测切换按钮何时被选中,甚至可以从<body>-element 元素中检测。当弹出窗口可见时,我们可以利用此功能来防止溢出/滚动:

@media (max-width: 767px) {
  body:has(.menu-toggle:checked) {
    overflow: hidden;
  }
}
Enter fullscreen mode Exit fullscreen mode

如果您旋转(大)手机,菜单将切换到桌面版本。

如果你的 iPhone 有“刘海”,菜单就会“进入”刘海里。我们可以使用env()-function 和 来解决这个问题safe-area-inset。首先,我们创建两个分别带有 block- 和 inline-padding 的变量:

header {
  --menu-pb: .75em;
  --menu-pi: 1.5em;
}
Enter fullscreen mode Exit fullscreen mode

...稍后我们将定义填充:

header {
  padding:
    var(--menu-pb)
    calc(env(safe-area-inset-right) 
    + var(--menu-pi))
    var(--menu-pb)
    calc(env(safe-area-inset-left)
    + var(--menu-pi));
}
Enter fullscreen mode Exit fullscreen mode

现在,当您旋转手机时,如果手机有凹口,则会在线添加额外的填充!

演示


可访问性问题

虽然上面的菜单无需任何 JavaScript即可正常工作,但切换按钮复选框是一种黑客技术,无法与屏幕阅读器很好地配合使用。

让我们id向弹出窗口添加一个,并将替换<label>为:

<button
  class="menu-toggle"
  aria-label="Toggle flyout menu"
  aria-expanded="false"
  aria-controls="flyout-menu">
</button>
Enter fullscreen mode Exit fullscreen mode

然后添加一小段 JS 代码:

const toggle = document.querySelector(
  '.menu-toggle');
toggle.addEventListener('click', () => {
  toggle.setAttribute(
    'aria-expanded', document.body
    .classList.toggle('menu-open')
    );
})
Enter fullscreen mode Exit fullscreen mode

menu-open这段代码切换了body 元素的class 。我们将使用状态(无论 class 是否设置)作为aria-pressed-attribute。

我们之前使用过的所有地方:checked,现在都应该使用menu-open

.menu-open .menu-flyout { translate: 0; }
Enter fullscreen mode Exit fullscreen mode

演示


演示中涉及的内容(过渡、夹紧间隙、悬停等)比我在本文中展示的要多得多。

在 Codepen 上打开它们以查看全宽桌面视图 - 分叉它们 - 并使用它们进行操作。

请在评论中告诉我你的想法。


摄影:Brett Sayles

鏂囩珷鏉ユ簮锛�https://dev.to/madsstoumann/mobile-to-desktop-menu-in-2-lines-of-css-4ikn
PREV
使用 Typescript 的 useContext()
NEXT
前端安全:npm-audit 和脚本完整性