阅读 215

用现代化的 CSS 技术来替代你的浮动布局吧

每次当我遇到一个组件需要用到浮动布局的时候,我都会问问我自己:真的有必要使用浮动布局吗?我开始注意到了几个确实不必要使用position: absolute的例子。我觉得这件事还挺有趣的,因为我决定将我在前端项目中遇到的几个例子整理成文档记录下来。

在这篇文章里,我会讲述几个使用了浮动布局,但实际上不必要的例子。

介绍

如果我们回到五年前,CSS 弹性布局还是一个比较新的概念,我们往往要对它进行很多的降级处理。CSS 网格布局在那个时候甚至都还不支持。最终,我们会使用 CSS 的定位来实现我们想要的效果。然而这其中的一些例子,在今天我们已然可以使用弹性布局或者网格布局来实现了。

几天以前,我们组正在开发一个前端的登陆页面,然后我的同事问了我一个布局问题。而这个问题恰好又是因为postion: absolute引起的,我尝试帮助她,最后我们甚至没有用到浮动布局就解决了这个问题!就在那个时刻,那件事启发了我:我应该把这种问题记录下来写成文章——这也恰恰是我正在做的事情。

一些例子

卡片覆盖

当我们有一个卡片,然后需要一些文字浮动在上面的时候,我们通常第一时间想到的就是使用position: absolute来解决这个问题。但有了 CSS Grid 之后,我们就不再需要这么做了:

上面这是一个典型的“文字浮于卡片”的样式,下面是它的HTML代码:

<article class="card">
    <div class="card__thumb">
        <img src="assets/mini-cheesecake.jpg" alt="">
    </div>
    <div class="card__content">
        <h2><a href="#">Title</a></h2>
        <p>Subtitle</p>
    </div>
</article>
复制代码

为了让文字内容能够浮在卡片上,我们需要让.card__content浮动:

.card {
    position: relative;
}

.card__content {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(to top, #000, rgba(0, 0, 0, 0) bottom/100% 60% no-repeat;
    padding: 1rem;
}
复制代码

当我们决定使用position: absolute来实现这个效果的时候,这么做是完全正确的。那么还有没有更简单的方法呢?让我们来看看:

第一步我们需要给整个卡片组件一个 display: grid。我们先不需要设置任何的行或者列。

.card {
    position: relative;
    display: grid;
}

.card__content {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}
复制代码

img

CSS Grid 布局会默认将内容变成行元素。在上面这个卡片组件中,我们有两个主要的元素(背景图和文字内容),所以我们有两行。

如果要将内容重叠到图片上面,我们需要将它们两个元素放在同一个**网格单元(grid area)**中。

.card__thumb,
.card__content {
    grid-column: 1/2;
    grid-row: 1/2;
}
复制代码

我们可以用grid-area缩写来更优雅地实现它:

.card__thumb,
.card__content {
    grid-area: 1/2;
}
复制代码

img

最后,我们也可以使用grid-area: 1/-1来替代。因为-1在 Grid 布局中代表了最后一行和最后一列,因此这个缩写总能找到布局中的最后一行和最后一列元素。

卡片标签

基于前面的例子,我们现在想拓展一下这个demo。我们想给卡片的左上角加一个“标签”。为了实现这个效果,我们会用到相同的技巧,也就是grid-area: 1/-1,但可以看到下图,我们并不想这个标签覆盖到这个容器上:

img

为了修复这个问题,我们需要设置一些这个元素相对容器的对齐属性:

.card__tag {
	align-self: start;
  justify-self: start;
  /* 其他的样式属性 */
}
复制代码

我们可以看到,这个标签已经根据它的父元素浮动了,而且我们完全没有用到position: absolute

Hero Section

注:在网页开发中 Hero Section 是一个特殊名词,它特指人们打开你的网站第一眼看到的东西。

另一个完美的例子就是:我们常常在 hero section 里使用文字浮在图片上的效果。

img

这跟卡片的例子有点点像,但是在文字内容的样式代码方面会有一些不一样。在我们深入探究现代化的CSS实现之前,请你花上几分钟思考一下,我们怎么用浮动布局来实现它。

现在我们有三个图层:

  • 图片
  • 透明渐变的遮罩
  • 文本

img

我们有几种方式可以实现它,如果图片只是为了单纯的装饰性作用的话,我们可以使用 background-image 属性,否则,我们就要使用一个 <img> 标签。

.hero {
    position: relative;
    min-height: 500px;
}

.hero__thumb {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* 重叠上去的元素 */
.hero:after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: #000;
    opacity: 0.5;
}

.hero__content {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    transform: translate(-50%, -50%);
    text-align: center;
}
复制代码

上面是我们使用浮动布局来实现 hero section 的代码。但是,现在我们想用更优雅的方式来实现一哈,让我们来看看更现代化的做法吧。

首先,我们需要给整个 hero 元素套上一个 display: grid 属性。然后,就像在卡片例子中做的一样,我们给 hero 元素的所有子元素都套上一个 grid-area: 1/-1(让他们挤在一个网格元素中)。

img

注意,我们现在需要修正一下 hero section 的高度,才能让 .hero_thumb 这个元素生效。(因为.hero_thumb这个子元素使用的是 height: 100%,这个属性必须在父元素有固定的高度的时候才能生效,而我们之前的 min-height: 100px; 并不是一个固定的高度。)

.hero {
    display: grid;
    height: 500px;
}

.hero__content {
    z-index: 1; /* [1] */
    grid-area: 1/-1;
    display: flex;
    flex-direction: column;
    margin: auto; /* [2] */
    text-align: center;
}

.hero__thumb {
    grid-area: 1/-1;
    object-fit: cover; /* [3] */
    width: 100%;
    height: 100%;
    min-height: 0; /* [4] */
}

.hero:after {
    content: "";
    background-color: #000;
    opacity: 0.5;
    grid-area: 1/-1;
}
复制代码

我想着重讲一下我标记的几个注释所在行的代码:

  1. 我们可以直接在子元素上使用z-index属性,不需要再给父元素添加position: relative了!
  2. 既然.hero__content是一个网格元素,使用margin: auto能够让他水平垂直居中。
  3. 别忘记了在处理图片的时候加上object-fit: cover
  4. 我在北京图片上使用了min-height:0来保证如果某人给 hero section 配置了一张大图的话,这张大图不会超出 hero section 的范围。如果对这个细节感兴趣,可以阅读我的文章:minimum content size in CSS grid [1]。

注意到了吗,现在我们的代码已经变得更优雅清晰啦。

CSS display: contents

我感觉这是我在现实工作中遇到的唯一一个能用上display: content属性的地方,请你看看下面这个布局图:

img

这是它的HTML代码:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
    <img src="recipe.jpg" alt="">
</div>
复制代码

目前为止没有什么特别的,但在移动端上,我们想要它响应式地变成这样:

img

请你注意,布局上的图片被插入到了标题和描述中间。如果是你,你要怎么解决这个问题呢?可能最先想到的,是会去更改HTML的顺序:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <img src="recipe.jpg" alt="">
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
</div>
复制代码

在移动端上,更改HTML的行为成功了。 在桌面端,我们要将这个<img>标签浮动在右侧。但其实我们有更直接的解决方案,那就是用display: contents

让我们回到最初的HTML代码:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
    <img src="recipe.jpg" alt="">
</div>
复制代码

注意到我们的文字内容是被包裹在 .hero__content 容器中的。我们怎么样能够告诉浏览器,我们想要<h2>, <p>还有<a>成为<img>的兄弟节点呢?这时候,轮到display: contents登场了。

.hero__content {
	display: contents;
}
复制代码

通过给.hero_content添加display: content,这个容器节点变成了一个隐藏的幽灵节点。浏览器实际上会像下面的代码一样解析HTML:

<div class="hero">
    <h2><!-- Title --></h2>
    <p><!-- Desc --></p>
    <a href="#">Order now</a>
    <img src="recipe.jpg" alt="">
</div>
复制代码

然后我们只需要添加下面的CSS:

.hero {
    display: flex;
    flex-direction: column;
}

.hero__content {
    display: contents;
}

.hero h2,
.hero img {
    order: -1;
}
复制代码

如果我们使用得当,display: contents能够成为一个强有力的技巧,来实现几年前很麻烦才能做到的事情。

当然,上面的例子我们还需要处理一下桌面端的布局:

@media (min-width: 750px) {
    .hero {
        flex-direction: row;
    }
    
    .hero__content {
        display: initial;
    }
    
    .hero h2,
    .hero img {
        order: initial;
    }
}
复制代码

将卡片组件重新排列

img

在这个例子中,我们有一个卡片的不同形态,一种是标题在图片上面,一种是标题在图片下面。

我们来看一下这个例子的HTML:

<article class="card">
    <img src="thumb.jpg" alt="">
    <div class="card__content">
        <h3>Title</h3>
        <p>Description</p>
        <p>Actions</p>
    </div>
</article>
复制代码

注意到,我们只有两个子元素,一个是图片,一个是内容文本。我们能否通过这段HTML代码,让<h3>位于卡片组件的最上端呢?

我们能通过让它浮动来实现:

.card {
    position: relative;
    padding-top: 3rem;
    /* 为标题留出适应空间 */
}

.card h3 {
    position: absolute;
    left: 1rem;
    top: 1rem;
}
复制代码

问题是,当我们的标题文本太长的时候,这段代码就要需要再次调整了。

之所以有这个问题的存在,是因为标题文本脱离了正常的文档流。所以浏览器在渲染组件卡片的时候并不关心标题文本是长是短。这个时候,如果我们用diplay: contents的话,就能更好的解决这个问题了:

.card {
    display: flex;
    flex-direction: column;
    padding: 1rem;
}

.card__content {
    display: contents;
}

.card h3 {
    order: -1;
}
复制代码

这样的代码能够让我们控制所有的子元素,并且让它们都拥有弹性的能力,然后通过order属性来控制它们的展示顺序。

img

这里还有一个小问题,根据设计图,我们中间的图片应该跟边框相连(占满整个宽度),我们这样来解决这个问题:

.card img {
	width: calc(100% + 2rem);
  margin-left: -1rem;
}
复制代码

居中

当我们谈论使一个元素居中的时候,你通常会看到许多关于它的段子,这也侧面说明了这件事有多复杂。但到了现在,其实使一个元素居中已经是最简单不过的事情了。

**请不要再使用position: absolute搭配transform来做了。**随便举个例子,我们直接用margin: auto就可以让一个 flex 元素水平垂直居中。

.card {
    display: flex;
}

.card__image {
    margin: auto;
}
复制代码

img

以前我也写过一篇居中的完全指南。[2]

图片宽高比

新的CSS属性aspect-ratio早就已经在大多数浏览器上被支持啦。有时候,我们通常使用 padding 这种 hack 的技巧来使得一个容器保持一定的宽高比。

举个🌰:

.card__thumb {
  position: relative;
  padding-top: 75%;
}

.card__thumb img {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
复制代码

有了aspect-ratio,就容易多了:

.card__thumb {
  position: relative;
  aspect-ratio: 4/3;
}
复制代码

你也能在这篇文章中学习更多关于它的知识。[3]

有时候,用Position: Absolute是更好的方案

img

看一看这个例子,我们有一段内容需要叠在一个卡片背景上。简单来说,我们有两种方案来实现它:

  1. 对卡片背景使用position: absolute
  2. 给内容容器一个负的margin。

下面我们每种都试一下,看看哪种更适合这个场景:

使用浮动布局

img

使用这个方案,我们可以添加一个浮动的矩形元素,然后让卡片 content 添加一个padding: 1rem

.card__cover {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 50px;
}

.card__content {
    padding: 1rem;
}
复制代码

这样写的话,即使这个背景被移除了。我们也不需要改动其他的代码,整个布局看起来也还不错:

img

使用负边距

用这个方案,我们不需要让任何的元素浮动,只是让内容元素给一个负的边距:

.card__content {
    padding: 1rem;
    margin-top: -1rem;
}
复制代码

当卡片的背景展示完整的时候,效果是 ok 的。但如果背景没加载出来或者被移除了,整个卡片就会变形:

img

你能看到这个头像跟它的父元素(卡片容器)的偏移,为了解决这个问题,当背景没展示出来的时候,我们需要改变它的CSS代码。

.card--no-cover .card__content {
    margin-top: 0;
}
复制代码

结果我们发现,使用position: absolute反而成了一个更好的方案,因为它使得我们不需要为额外的场景写更多的兼容代码。

拓展阅读

  • Ryan Mulligan 在关于如果使用 CSS Grid 来将内容重叠到元素上写了一篇很好的文章。[4]
  • Stephanie Eckles 也写了很漂亮的例子,如何利用 CSS Grid 来搭建一个 hero section。[5]

🔗相关链接:

[1]: ishadeed.com/article/min…

[2]: ishadeed.com/article/lea…

[3]: ishadeed.com/article/css…

[4]: css-tricks.com/positioning…

[5]: moderncss.dev/3-popular-w…

文章分类
前端