响应式布局,减少媒体查询

Avatar of Temani Afif
Temani Afif on

DigitalOcean 提供适合您旅程各个阶段的云产品。立即开始使用 $200 免费积分!

我们无法不谈论响应式设计,就谈论 web 开发。这在如今已经成为常识,并且已经存在多年了。媒体查询 是响应式设计的一部分,并且不会消失。自媒体查询出现以来(从字面上来说是几十年前),CSS 已经发展到足以提供许多技巧来帮助我们大幅减少媒体查询的使用。在某些情况下,我将向您展示如何用一个 CSS 声明来替换多个媒体查询。这些方法可以减少代码量,更易于维护,并且与手头的内容更加紧密地结合在一起。

让我们首先看看一些广泛使用的方法,这些方法可以在没有媒体查询的情况下构建响应式布局。这里没有什么惊喜——这些方法与 flexboxgrid 相关。

使用 flexflex-wrap

实时演示

在上面的演示中,flex: 400px 为网格中的每个元素设置了一个基本宽度,等于 400px。如果当前行没有足够的空间容纳元素,则每个元素都会换行。同时,每行上的元素会增长/拉伸以填充容器中剩余的任何空间(如果该行无法容纳另一个 400px 元素,并且如果确实可以挤进另一个 400px 元素,则它们会缩小到 400px)。

我们也要记住,flex: 400px 是一个简写形式,等同于 flex: 1 1 400pxflex-grow: 1flex-shrink: 1flex-basis: 400px)。

我们目前的状况

  • ✔️ 只有两行代码
  • ❌ 页脚中元素宽度一致
  • ❌ 控制每行项目数量
  • ❌ 控制项目换行的时间

使用 auto-fitminmax

实时演示

与之前的方法类似,我们正在设置一个基本宽度——感谢 repeat(auto-fit, minmax(400px, 1fr))——并且如果我们的项目有足够的空间,它们就会换行。但是,这次我们正在使用 CSS Grid。这意味着每行上的元素也会增长以填充剩余的任何空间,但与 flexbox 配置不同的是,最后一行保持与其他元素相同的宽度。

因此,我们改进了一个需求并解决了另一个需求,但也引入了一个新问题,因为我们的项目无法缩小到低于 400px,这可能会导致一些溢出。

  • ✔️ 只有 一行 代码
  • ✔️ 页脚中元素宽度一致
  • ❌ 控制每行项目数量
  • ❌ 项目增长,但不缩小
  • ❌ 控制项目换行的时间

我们刚刚查看的两种技术都很不错,但我们也发现它们有一些缺点。但是我们可以通过一些 CSS 技巧来克服这些缺点。

控制每行项目数量

让我们以第一个示例为例,将 flex: 400px 更改为 flex: max(400px, (100% - 20px)/3)

调整屏幕大小并注意,即使在超宽屏幕上,每行也永远不会超过三个项目。我们已将每行限制为最多三个元素,这意味着每行在任何给定时间只包含一到三个项目。

让我们分解一下代码

  • 当屏幕宽度增加时,我们的容器宽度也会增加,这意味着 100%/3 在某个时刻会大于 400px
  • 由于我们使用 max() 函数作为宽度,并在其中将 100% 除以 3,因此单个元素的最大值仅为容器总宽度的三分之一。因此,每行最多有三个元素。
  • 当屏幕宽度较小时,400px 会占据主导地位,我们就会得到初始行为。

您可能还会问:公式中的 20px 值到底是什么?

它是网格模板的 gap 值的两倍,即 10px 乘以二。当一行上有三个项目时,元素之间有两个间隙(中间元素的左侧和右侧各有一个),因此对于 N 个项目,我们应该使用 max(400px, (100% - (N - 1) * gap)/N)。是的,我们需要在定义宽度时考虑 gap,但不要担心,我们仍然可以优化公式以将其删除!

我们可以使用 max(400px, 100%/(N + 1) + 0.1%)。逻辑是:我们告诉浏览器每个项目的宽度等于 100%/(N + 1),因此每行 N + 1 个项目,但我们添加了一个很小的百分比(0.1%)——因此其中一个项目会换行,最终只有 N 个项目在一行上。很聪明,对吧?不必再担心间隙了!

现在我们可以控制每行项目的**最大**数量,这为我们提供了对每行项目数量的部分控制。

同样的方法也可以应用于 CSS Grid 方法。

请注意,这里我引入了 自定义属性 来控制不同的值。

我们越来越接近了!

  • ✔️ 只有 一行 代码
  • ✔️ 页脚中元素宽度一致
  • ⚠️ 对每行项目数量的 部分控制
  • ❌ 项目增长,但不缩小
  • ❌ 控制项目换行的时间

项目增长,但不缩小

我们之前提到,如果基本宽度大于容器宽度,使用网格方法会导致溢出。为了克服这个问题,我们将更改

max(400px, 100%/(N + 1) + 0.1%)

…为

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

分解一下

  • 当屏幕宽度较大时,400px 被钳制到 100%/(N + 1) + 0.1%,保持我们对每行项目最大数量的控制。
  • 当屏幕宽度较小时,400px 被钳制到 100%,因此我们的项目永远不会超过容器宽度。

我们越来越接近了!

  • ✔️ 只有 一行 代码
  • ✔️ 页脚中元素宽度一致
  • ⚠️ 对每行项目数量的 部分控制
  • ✔️ 项目增长和缩小
  • ❌ 控制项目换行的时间

控制项目换行的时间

到目前为止,我们无法控制元素从一行换到另一行的时机。我们并不真正知道它何时发生,因为这取决于许多因素,比如基本宽度、间隙、容器宽度等等。为了控制这一点,我们将更改最后一个 clamp() 公式,从这个

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

…为

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)

我能听到你在尖叫,说这个公式看起来很疯狂,但是请耐心点。其实它比你想象的要简单。下面是发生的事情

  • 当屏幕宽度 (100vw) 大于 400px 时,(400px - 100vw) 会产生一个负值,并且会被钳制到 100%/(N + 1) + 0.1%,这是一个正值。这使我们每行有 N 个项目。
  • 当屏幕宽度 (100vw) 小于 400px 时,(400px - 100vw) 是一个正值,并乘以一个被钳制到 100% 的大值。这会导致每行有 一个 全宽度元素。
实时演示

嘿,我们创建了第一个没有真正媒体查询的媒体查询!通过 clamp() 公式,我们正在将每行项目数量从 N 更新为 1。需要注意的是,400px 在这种情况下充当断点。

那么:从每行 N 个项目到每行 M 个项目?

我们可以通过更新容器的钳制宽度来做到这一点

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%/(M + 1) + 0.1%)

我想你现在可能已经明白了诀窍。当屏幕宽度大于 400px 时,我们会进入第一个规则(每行 N 个项目)。当屏幕宽度小于 400px 时,我们会进入第二个规则(每行 M 个项目)。

实时演示

完成了!现在我们可以控制每行项目数量,以及该数量何时应该更改——只需使用 CSS 自定义属性和一个 CSS 声明即可。

  • ✔️ 只有 一行 代码
  • ✔️ 页脚中元素宽度一致
  • ✔️ 完全控制每行项目数量
  • ✔️ 项目增长和缩小
  • ✔️ 控制项目换行的时间

更多示例!

控制两个值之间的项目数量很好,但对多个值进行控制会更好!让我们尝试从每行 N 个项目到每行 M 个项目,再到每行一个项目。

我们的公式变为

clamp(clamp(100%/(N + 1) + 0.1%, (W1 - 100vw)*1000,100%/(M + 1) + 0.1%), (W2 - 100vw)*1000, 100%)

clamp() 中使用 clamp()?没错,这会让代码变得很长很复杂,但仍然很容易理解。请注意其中的 W1W2 变量。由于我们在三个值之间更改每行的项目数量,因此需要两个“断点”(从 NM,以及从 M1)。

以下是发生的事情

  • 当屏幕宽度小于 W2 时,我们将限制为 100%,即**每行一个项目**。
  • 当屏幕宽度大于 W2 时,我们将限制为第一个 clamp()
  • 在第一个限制中,当屏幕宽度小于 W1 时,我们将限制为 100%/(M + 1) + 0.1%),即**每行 M 个项目**。
  • 在第一个限制中,当屏幕宽度大于 W1 时,我们将限制为 100%/(N + 1) + 0.1%),即**每行 N 个项目**。

我们使用一个 CSS 声明创建了两个媒体查询!不仅如此,我们还可以通过 CSS 自定义属性调整该声明,这意味着我们可以为不同的容器设置不同的断点和不同的列数。

上面的例子中我们有多少个媒体查询?太多数不过来,但我们不会就此止步。我们可以嵌套另一个 clamp() 来创建更多媒体查询,从而实现**从 N 列到 M 列到 P 列到一列**的布局。(😱)

clamp(
  clamp(
    clamp(100%/(var(--n) + 1) + 0.1%, (var(--w1) - 100vw)*1000,
          100%/(var(--m) + 1) + 0.1%),(var(--w2) - 100vw)*1000,
          100%/(var(--p) + 1) + 0.1%),(var(--w3) - 100vw)*1000,
          100%), 1fr))
N 列到 M 列到 P 列到 1

正如我在本文开头提到的,我们创建了一个响应式布局,它不使用任何媒体查询,只使用一个 CSS 声明——当然,这是一个很长的声明,但仍然算作一个声明。

以下是我们所做的简要总结

  • ✔️ 只有 一行 代码
  • ✔️ 页脚中元素宽度一致
  • ✔️ 完全控制每行项目数量
  • ✔️ 项目增长和缩小
  • ✔️ 控制项目换行的时间
  • ✔️ 使用 CSS 自定义属性易于更新

让我们模拟容器查询

每个人都对容器查询感到兴奋! 它们之所以很酷,是因为它们考虑的是元素的宽度,而不是视窗/屏幕的宽度。其理念是,元素可以根据其父容器的宽度进行调整,从而更精细地控制元素对不同上下文的响应。

在撰写本文时,容器查询还没有得到任何地方的正式支持,但我们当然可以使用我们的策略来模仿它们。如果我们在整个代码中将 100vw 替换为 100%,那么所有内容都将基于 .container 元素的宽度,而不是视窗宽度。就这么简单!

调整下面容器的大小,看看神奇的效果

列数会根据容器宽度进行变化,这意味着我们正在模拟容器查询!我们实际上只是通过将视窗单位更改为相对百分比值来实现这一点。

更多技巧!

既然我们可以控制列数,那么让我们来探索更多技巧,这些技巧可以让我们根据屏幕大小(或元素大小)创建条件 CSS。

条件背景颜色

不久前,StackOverflo 上的某个人问过 是否可以根据元素的宽度或高度更改其颜色。许多人说这是不可能的,或者需要使用媒体查询。

但我发现了一个无需使用媒体查询就能实现此功能的技巧

div {
  background:
   linear-gradient(green 0 0) 0 / max(0px,100px - 100%) 1px,
   red;
}
  • 我们有一个线性渐变层,其宽度等于 max(0px,100px - 100%),高度等于 1px。高度并不重要,因为渐变默认情况下会重复。此外,它是一个单色渐变,因此任何高度都可以满足要求。
  • 100% 指的是元素的宽度。如果 100% 计算得出的值大于 100px,则 max() 会返回 0px,这意味着渐变不会显示,但用逗号分隔的 red 背景会显示。
  • 如果 100% 计算得出的值小于 100px,则渐变会显示,我们将会得到一个 green 背景。

换句话说,我们根据元素的宽度与 100px 的比较创建了一个条件!

此演示在撰写本文时支持 Chrome、Edge 和 Firefox。

我们可以通过重新排列 1px 值的位置来根据元素的高度而不是宽度来实现相同的逻辑:1px max(0px,100px - 100%)。我们还可以通过使用 vhvw 而不是 % 来考虑屏幕尺寸。我们甚至可以添加更多渐变层来使用两种以上的颜色。

div {
  background:
   linear-gradient(purple 0 0) 0 /max(0px,100px - 100%) 1px,
   linear-gradient(blue   0 0) 0 /max(0px,300px - 100%) 1px,
   linear-gradient(green  0 0) 0 /max(0px,500px - 100%) 1px,
   red;
}

切换元素的可见性

为了根据屏幕大小显示/隐藏元素,我们通常会使用媒体查询,并在其中放置一个经典的 display: none。以下是一种模拟相同行为的方法,但无需使用媒体查询

div {
  max-width: clamp(0px, (100vw - 500px) * 1000, 100%);
  max-height: clamp(0px, (100vw - 500px) * 1000, 1000px);
  overflow: hidden;
}

根据屏幕宽度 (100vw),我们将 max-heightmax-width 限制为 0px 值(表示元素隐藏),或者限制为 100%(表示元素可见,且永远不会大于全宽)。我们之所以没有使用百分比作为 max-height,是因为它会失败。因此,我们使用了较大的像素值 (1000px)。

请注意,绿色元素在小屏幕上会消失

需要注意的是,此方法并不等同于切换 display 值。这更像是让元素的尺寸变为 0×0,从而使其不可见。它可能并不适合所有情况,因此请谨慎使用!这更像是一个用于装饰性元素的技巧,我们不会遇到可访问性问题。Chris 曾经写过关于如何负责任地隐藏内容

重要的是要注意,我在 clamp()max() 中使用的是 0px,而不是 0。后者会使属性失效。我不会深入研究这一点,但我在一个与这个怪癖相关的 Stack Overflow 问题中回答过这个问题,如果你想了解更多细节,可以查看一下。

更改元素的位置

当我们处理固定位置或绝对位置的元素时,以下技巧很有用。这里不同的是,我们需要根据屏幕宽度更新位置。与前面的技巧一样,我们仍然依赖于 clamp() 和一个看起来像这样的公式:clamp(X1,(100vw - W)*1000, X2)

基本上,我们将根据 100vw - W 的差值在 X1X2 值之间切换,其中 W 是模拟断点的宽度。

举个例子,我们希望将一个 div 放在左侧边缘 (top: 50%; left:0),当屏幕大小小于 400px 时,并在其他地方 (例如 top: 10%; left: 40%) 放置它,否则。

div {
  --c:(100vw - 400px); /* we define our condition */
  top: clamp(10%, var(--c) * -1000, 50%);
  left: clamp(0px, var(--c) * 1000, 40%);
}
实时演示

首先,我使用 CSS 自定义属性定义了条件,以避免重复。请注意,我还将其与之前看到的背景颜色切换技巧一起使用——我们既可以使用 (100vw - 400px),也可以使用 (400px - 100vw),但请注意后面的计算,因为两者符号不同。

然后,在每个 clamp() 中,我们始终从每个属性的最小值开始。不要错误地认为我们需要先放置小屏幕的值!

最后,我们定义每个条件的符号。我选择了 (100vw - 400px),这意味着当屏幕宽度小于 400px 时,此值将为值,而当屏幕宽度大于 400px 时,此值将为值。如果需要 clamp() 的最小值在 400px 以下被考虑在内,那么我不会对条件的符号做任何修改(保持其为正值),但如果需要在 400px 以上考虑最小值,那么我需要反转条件的符号。这就是你看到 (100vw - 400px)*-1000top 属性一起使用的原因。

好的,我明白了。这不是最容易理解的概念,所以让我们反过来推理,并跟踪我们的步骤,以便更好地理解我们正在做什么。

对于 top,我们有 clamp(10%,(100vw - 400px)*-1000,50%),所以…

  • 如果屏幕宽度 (100vw) 小于 400px,那么差值 (100vw - 400px) 将为值。我们将其乘以另一个大的负值 (本例中为 -1000),以获得一个大的值,该值被限制为 50%:这意味着当屏幕大小小于 400px 时,我们将得到 top: 50%
  • 如果屏幕宽度 (100vw) 大于 400px,那么我们将得到:top: 10%

相同的逻辑适用于我们在 left 属性中声明的内容。唯一的区别是我们乘以 1000 而不是 -1000

这里有一个秘密:你并不真的需要所有的数学运算。你可以不断尝试,直到找到合适的数值,但为了本文的解释,我需要以一种能带来一致行为的方式来解释事情。

需要注意的是,像这样的技巧适用于任何接受长度值的属性(paddingmarginborder-widthtranslate 等)。我们并不局限于更改 position,还可以更改其他属性。

演示!

你们中大多数人可能想知道,这些概念是否能在现实世界的用例中实际应用。让我向你们展示一些例子,它们(希望)能说服你们,这些概念是可行的。

进度条

背景颜色改变的技巧可以制作一个很棒的进度条,或者任何需要根据进度显示不同颜色的类似元素。

此演示在撰写本文时支持 Chrome、Edge 和 Firefox。

那个演示是一个非常简单的例子,我在其中定义了三个范围

  • 红色: [0% 30%]
  • 橙色: [30% 60%]
  • 绿色: [60% 100%]

没有疯狂的 CSS 或 JavaScript 来更新颜色。一个“神奇”的背景属性使我们能够拥有一个动态颜色,它根据计算的值而改变。

可编辑内容

通常会给用户提供一种编辑内容的方式。我们可以根据输入的内容来更新颜色。

在下面的例子中,当输入超过三行文本时,我们会得到一个黄色的“警告”,如果超过六行,则会得到一个红色的“警告”。这可以是一种减少需要检测高度然后添加/删除特定类的 JavaScript 的方法。

此演示在撰写本文时支持 Chrome、Edge 和 Firefox。

时间轴布局

时间轴是可视化时间关键时刻的绝佳模式。这种实现使用了三个技巧来实现一个没有媒体查询的时间轴。一个技巧是更新列数,另一个是在小屏幕上隐藏一些元素,最后一个是更新背景颜色。同样,没有媒体查询!

当屏幕宽度低于 600px 时,所有伪元素都会被移除,将布局从两列改为一列。然后颜色从蓝色/绿色/绿色/蓝色模式更新为蓝色/绿色/蓝色/绿色模式。

响应式卡片

这是一个响应式卡片方法,其中 CSS 属性根据视窗大小更新。通常,我们可能期望布局从大屏幕上的两列过渡到小屏幕上的单列,其中卡片图片叠放在内容的上方或下方。然而,在这个例子中,我们改变了图片的位置、宽度、高度、填充和边框半径,以获得一个完全不同的布局,其中图片位于卡片标题旁边。

气泡

需要一些好看的评价来展示你的产品或服务吗?这些响应式气泡几乎适用于任何地方,即使没有媒体查询。

固定按钮

你知道那些有时固定在屏幕左侧或右侧边缘的按钮吗?通常用于链接到联系方式或调查?我们可以在大屏幕上使用一个这样的按钮,然后把它转换成一个固定在小屏幕右下角的持续圆形按钮,以方便点击。

固定提醒

再演示一个,这次是关于那些 GDPR cookie 通知的东西。

结论

媒体查询自“响应式设计”这个词诞生以来,一直是响应式设计的一个核心要素。虽然它们肯定不会消失,但我们介绍了一堆更新的 CSS 功能和概念,它们使我们能够减少对媒体查询的依赖,以创建响应式布局。

我们研究了 Flexbox 和 Grid、clamp()、相对单位,并将它们结合在一起,做各种各样的事情,从根据容器宽度改变元素的背景,到在特定屏幕尺寸下移动位置,甚至模拟尚未发布的容器查询。令人兴奋的事情!而且这一切都没有在 CSS 中使用任何 @media

这里的目标不是消除或替换媒体查询,而是更多地优化和减少代码量,尤其是 CSS 已经发展了很多,现在我们有一些强大的工具来创建条件样式。换句话说,看到 CSS 功能集在让我们的前端开发工作更容易的同时,赋予我们控制设计行为的能力,这真是太棒了。