不使用 Media Queries 的自适应 CSS - 众成翻译

1,212 阅读9分钟
原文链接: zcfy.cc

A variety of screens with different responsive images

虽然标题说不用media queries,但我还是要在开头讲清楚,本文并不是要大家丢掉media queries,也不是要抨击media queries。media queries实际上非常有用,我在各种地方都会用到。但media queries并不能解决所有的自适应的设计问题。

常常有一种需求是根据元素所在的组件本身的大小来决定元素的排列,而不是整个视窗的宽高。要解决这类问题,element queries的概念诞生了。然而,element queries只停留在概念上,Mat Marquis 也针对这个概念提出了一些问题,并且称之为container queries。不过可惜这些都只停留在概念上。

也许未来有一天,这些概念会成为现实,不过与此同时,我也来分享一些小技巧,你现在就可以用来处理某一类的问题。

flex-wrap的Flexbox

Flex-wrap可以解决很多和容器大小相关的自适应性问题。例如,如果有足够的空间,那就把两个元素并排陈列;如果没有足够空间,就把这两个元素上下排列。下面这个例子里我们就能看到这个情况:

参见CodePen上面的这个例子Responsive module - flexbox(by @SitePoint)。

没有什么华丽的技巧,就是用了flex-wrap的flexbox,非常完美。Flexbox可以用在很多种情况下,比如下面这个两列的情况,不过这个例子其实是我简化后的。核心部分其实非常简单:

<div class="container">
  <div class="img">...</div>
  <div class="content">...</div>
</div>

...

.container {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
}

.container .content,
.container .img {
  flex: 1 0 12em;
  /* Change 12em to your breakpoint. */
}

要正确使用flexbox,需要能理解flex-growflex-shrinkflex-basis的工作机制,我读到的Zeo Gillenwater 的这篇flexbox tips 对于理解这三者间的关系很有帮助。

神奇四剑客

使用width,min-width,max-width以及calc(也就是“神奇四剑客”)来创建一个基于分隔点的宽度变化解决方案,是Rémi Parmentier的点子。这最初是用来做自适应的邮件模板,然后就扩展到了平常的web页面,现在以及发展到,可以用来创建各种自适应的模块,参见 Thierry的CodePen。比如:

{
  min-width: 50%;
  width: calc((25em - 100%) * 1000);
  max-width: 100%;
  /* Change 25em to your breakpoint. */
}

这个方法在width是百分比的时候很有效,也就是元素宽度是所在容器宽度的百分比的时候有效。然后,calc函数将该值与所需的断点做减法,如果宽度值小于断点,那会得到一个非常大的正数,或者如果宽度值大于断点,那会得到一个非常大的负数,或者两者完全相等,得到零。但是有max-widthmin-width 对较大的正数或者负数进行封顶。

这里,我们的断点设的是25em。字号是16px的话,就相当于400px。如果容器宽度在400px及以上,也就是大于或等于断点,该元素的宽度就是0了,(400 - 400 = 0) * 1000 = 0或者(400 - 401 = -1) * 1000 = -1000

像这样的情况,min-width就会生效,元素的宽度就会变成50%。然而,如果容器的宽度是399px或者更低(也就是比断点更小),那么这个元素的宽度就是个很大的正数:(400 - 399 = 1) * 1000 = 1000

这样的话,max-width就生效,最后的结果就是100%。下面这个图也许可以帮你理解这个逻辑:

Diagram visualizing the Fab Four Technique

下面的几个demo使用了上面的这些技巧,通过不同的方式来切换元素的宽度以适应其容器宽度。

浮动图片:整体和部分的宽度处理

这个例子里,我会使用神奇四剑客和float属性,阐述如何根据容器宽度来切换图片占容器所有和部分宽度的方法。

看一下CodePen上这个例子:Responsive module - float(by @SitePoint)。

和上面的flexbox的例子很像,这个用例是让多个元素在更小的容器宽度上进行纵向排列,有足够空间时,左右浮动或者依次摆放。

浮动图片的显示和隐藏

接着上面的例子,我改了一下计算逻辑,去掉了min-width,好做一个开关。这个方法在容器宽度较小时可以有效节省可用空间。

参见CodePen上的Responsive module - float / hidden 这个例子(By @SitePoint)。

再多解释一下:

{
  /* Removed min-width since we want the width to be zero at this point and negative widths are treated as zero */
  /* Inverted the multiplier: */
  width: calc((25em - 100%) * -1000);
  max-width: 100%;
  /* Change 25em to your breakpoint. */
}

文字和图片:覆盖和堆叠

参见这个CodePen的例子Responsive module – overlaid / stacked(By )@SitePoint

和前例的代码相似,不过我又额外增加了一个div来把文字覆盖到图片上,但如果图片过小,那么反而文字会让图片不清晰,这时就把文字排放在图片下。这个技巧有些复杂,所以我在这解释一下。

.pull {
  /* Pull the text up over the image by this much: */
  margin-bottom: -10em;
}

这里,负的margin会把文字的位置上移,好让它能够覆盖图片区域。随着容器宽度变小,我需要让它消失,但由于此时没有min/max-width的属性限制,我们就不能用神奇四剑客的方法做到这一点。

不过好在有另一种方法,如果padding是百分比数值,那么它就是容器宽度的百分比,padding-toppadding-bottom都会影响元素的高度。了解这一点以后,我们就可以创造一个根据容器宽度变化而变化的padding-bottom

padding-bottom:calc((30em - 100%) * 1000);

我们不能给这个.pulldiv直接使用min/max-padding的类似方法,因为没有这样的属性;正确的做法是设法给这个padding加一个开关,这个开关是通过增加一个伪元素并控制这个伪元素的高度实现的,使其高度正好等于.pulldiv负边距,来达到控制高度的做法。

.pull {
  /* Pull the text up over the image by this much: */
  margin-bottom: -10em;
  /* Don't allow this container to be larger than the same amount: */
  max-height: 10em;
  /* and hide any overflow, just to be on the safe side: */
  overflow: hidden;
}

.pull::before {
  content: "";
  display: block;
  padding-bottom: calc((30em - 100%) * 1000);
  /* Change 30em to your breakpoint */
}

这个文字覆盖的效果是最终通过给伪元素加一个开关,来控制渐变色的背景部分。

.image::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  top: 0;

  /* Gradient to make the text more legible: */
  background-image: linear-gradient(to bottom, rgba(0,20,30,0) 0%,rgba(0,20,30,0) 50%,rgba(0,20,30,1) 100%);

  /* Extra .5% to prevent bleed due to rounding issues: */
  height: 100.5%;
  /* Toggle gradient overlay at the same breakpoint as the 'pull': */
  width: calc((30em - 100%) * -1000);
  /* Change 30em to your breakpoint */
  max-width: 100%;
}

列表截断

我能找到的这个终极技巧是受到了 Priority Plus pattern on CSS tricks 这篇文章的启发。不过我的这个方法没有那么复杂,也不需要用到JavaScript。

参见 CodePen上的 Truncating List (By @SitePoint)。

同样,这里用到“神奇四剑客”,区别只在与这里是用的容器高度而不是宽度。

<div class="outer">
  <div class="inner">
    <div class="item">...</div>
    ...
    <div class="control">...</div>
  </div>
</div>
...

.outer {
  height: 2.25em;
  overflow: hidden;
}

.outer:target {
  height: auto;
}

outer容器有一个固定的高度,并且会隐藏超出区域,除非这个元素有一个:target伪类。

.inner {
  display: flex;
  flex-wrap: wrap;
}

inner元素是一个flex-wrap的容器,因此其高度会可以根据其子元素内容增加,如果有超出了第一行的元素,就会被外层.outer的容器隐藏,因为.outer容器设置了overflow:hidden,这样就有了截断的列表效果。

.control {
  height: calc((2.25em - 100%) * -1000);
  max-height: 2.25em;
}

:target .control--open {
  display: none;
}

:target .control--close {
  display: block;
}

‘more/less’的控制是通过容器高度是否超过断点控制的,这里的断点就是外层容器的高度,:target伪类决定了元素是否可见。

CSS自动对齐文字

参见CodePen上的Responsive Text Align (by @SitePoint).

根据空间大小决定文字是左对齐还是居中对齐是非常有用的功能。Vijay Sharma找到了实现这个功能的简单方法。

福利篇:Flex-grow 9999 Hack

有一个很适合本文主题的小技巧,就是Joren Van Hee的这篇Flex-grow 9999 Hack

点赞篇:来自Vasilis van Gemert的好文《看,不需要Media Queries》

Vasilis van Gemert的演讲《看,不需要Media Queries》,让我有动力调研一下如何不使用或者更少的使用media query来完成自适应的设计,于是我写下了这篇文章。他的演讲很值得学习,包含了很多非常有用的想法,即便其中有一些想法不太适合本文的主题。

结论

如果没有element/container queries, 很多设计都不能实现。例如字体的颜色大小、行高、边距,box-shadows,内外边距,等等等等,这个列表太长了。根据父容器的尺寸调整以上这些属性,应该是需要做到的,但这件事恐怕还一时无法实现。然而,我还是希望我这里展示的一些技巧能对你有所帮助。

如果你想知道什么是根据Element Queries的设计,这篇文章可能对你有所帮助。

如果你想知道更多的内容,下面是一些有用的资源: