margin: auto 的魔法世界

avatar
公众号「 微医大前端技术 」

曹锋,医药支撑团队前端工程师。处女座,追求优雅的代码。

梦开始的地方

在 CSS 的世界里,各种居中问题可以说是时刻伴随着我们,其中 margin: auto 必须是当之无愧的童年记忆,甚至到了如今我们已经掌握了各种 CSS 的奇技淫巧,这段朴实无华的代码依旧占有一席之地,四个字来评价:yyds!让我们再来重温一下这段经典代码:

<style>
	.parent-panel {
    padding: 20px;
    background-color: darksalmon;
  }
  .main-panel {
    width: 60%;
    margin: auto;
    background-color: blanchedalmond;
    text-align-last: justify;
  }
</style>

<div class="parent-panel">
	<div class="main-panel">我居中了</div>
</div>

效果如下:

margin 居中

margin: auto 解决了 让一个 正常布局流(normal flow)固定宽度 元素 水平 居中 的问题,其原理就是:在 writing-mode: horizontal-tbdirection: ltr 的前提下,当我们给一个块元素设置 margin-left: auto ( margin-right: auto ) 时,其计算值为该块元素的父级在水平方向上的可用剩余空间,二者都设置了就均分剩余空间,自然就让该元素水平方向居中了。

上面说了让固定宽度块元素水平居中的方法,自然会想到居左和居右,居左就不用说了,居右该如何实现呢?按照上面对 margin 值为 auto 的计算原理的解释,我们不难想到一个解决方案:通过设置该元素的 margin-left: auto 让其 margin-left 占据父元素左侧所有的可用剩余空间,将其挤到父元素最右侧从而实现居右效果,代码如下:

<style>
	.parent-panel {
    padding: 20px;
    background-color: darksalmon;
  }
  .main-panel {
    width: 60%;
    margin-left: auto;
    background-color: blanchedalmond;
    text-align-last: justify;
  }
</style>

<div class="parent-panel">
	<div class="main-panel">我现在居右了</div>
</div>

效果如下:

margin 居右

通过上面两个例子可以看到, margin 值为 auto 可以很方便的控制一个固定宽度的块元素在水平方向上的对齐方式,但其实能做的事情很有限,主要是因为我们对该元素的约束很多: 固定宽度块元素水平方向 ,而且因为本身是块元素,所以在正常文档流中该行只会存在一个块元素,那么我们如何控制一些更为复杂的场景下元素的对其方式呢,比如垂直方向、多元素的场景等,大家一定都已经想到了,那就是 Flexbox 布局方式了。

Flexbox 带来的更多可能

Flexbox 布局想必大家都已经很熟悉并且在实际开发中大量使用了,它的出现为开发者提供了更优雅的布局方案,甚至是以前无法单独使用 CSS 解决的问题,具体的一些常用场景可以查看 这里(solved-by-flexbox) ,我们这里就不展开讨论各种布局方案,主要还是看看 Flexbox 遇到 margin: auto 会有哪些有趣的故事。

Flexbox 布局本身就自带很多属性用来控制子元素在 主轴(main axis)交叉轴(cross axis) 上的对其方式: align-items align-self justify-content 等,想必大家都已经大量使用过了,但是对于子元素在 主轴 上对其方式的控制,没有一个 justify-self 用来让单个元素可以对自己进行特殊处理,具体原因可以参考 这里 In CSS Flexbox, why are there no “justify-items” and “justify-self” properties? 。而且在一些特殊场景下 justify-content 的效果会有问题,下面都会一一讲到。接下来让我们看看一些实际布局效果该如何实现。

左右对齐

看看下面这种非常常见的布局方式:

一键三连

布局要求如下:所有子元素垂直方向居中对齐,水平方向上分成两个区域,左侧的「一键三连」主操作区,右侧的一些辅助操作区域,并且我们要求编写尽可能简洁的 html 代码,伪代码如下:

<ul class="operate-panel">
  <!-- 主操作区域 -->
  <li class="item">点赞</li>
  <li class="item">投币</li>
  <li class="item">收藏</li>
  <li class="item item-forward">转发</li>
  
  <!-- 辅助操作区域 -->
  <li class="item item-report">投诉</li>
  <li class="item">笔记</li>
  <li class="item">更多操作</li>
</ul>

我们首先想到的肯定是使用 Flexbox 进行布局,代码如下:

.operate-panel {
  display: flex;
  align-items: center;
}
.operate-panel .item + .item {
  margin-left: 2em;
}

上面的代码已经基本完成了布局要求,还差一个将辅助区域居右的该如何实现。一个思路是:让「转发」按钮 flex-grow: 1 将辅助区域挤到右边去,或者让「投诉」按钮 flex-grow: 1; text-align: right 也可以实现同样效果。代码如下:

.operate-panel .item.item-forward {
  flex-grow: 1;
}
/* or */
.operate-panel .item.item-report {
  flex-grow: 1;
  text-align: right;
}

但是上面代码有一个不好之处:元素宽度被拉伸了,这样可能会给元素内部的布局带来影响,比如要求元素最大宽度不能超过 100px ,超出部分显示省略号,所以我们的解决方法最好是不影响元素本身尺寸。

其实我们可以将左右两个区域分别用一个容器包裹起来,然后对着两个容器进行左右对齐布局,但是这样的话我们需要多写一些为了布局而存在的 html 标签,所以这个方案暂时不考虑。

那么接下来就看看 margin: auto 在这里能不能派上用场。我们先看看 margin: autoFlexbox 布局方式里的 计算方式是如何的:

Auto margins on flex items have an effect very similar to auto margins in block flow:

  • During calculations of flex bases and flexible lengths, auto margins are treated as 0.
  • Prior to alignment via justify-content and align-self, any positive free space is distributed to auto margins in that dimension.
  • Overflowing boxes ignore their auto margins and overflow in the end direction.

根据 规范(Aligning with 'auto' margins) 的定义,简单来说,在 flex items 上定义 margin: auto 的效果和上面讲到的在块元素上效果类似:占用父级剩余可用空间,但是有些不同的是, flex items 上的 margin: auto 不仅对水平方向有效,对垂直方向同样有效,OMG 用它用它用它!!! 直接看代码:

.operate-panel .item.item-forward{
	margin-right: auto;
}
/* or */
.operate-panel .item.item-report {
  margin-left: auto;
}

一个字:非常的优雅!这个问题就非常优雅的解决了,下一位。

带操作的页面顶部栏

标题有点绕,直接看效果图:

顶部栏

要求如下:左侧「返回」按钮居左,右侧操作区域居右,标题在 剩余可用空间 内水平 居中,所有子元素垂直方向居中。先看看 html 代码:

<ul class="header-panel">
  <li class="item">&lt; 返回</li>
  <li class="item item-title">我是标题</li>
  <li class="item">清单</li>
  <li class="item">搜索</li>
  <li class="item">发布</li>
</ul>

不出意料我们还是使用 flexbox 进行布局,主要的 CSS 代码如下:

.header-panel {
  display: flex;
  align-items: center;
}
.header-panel .item + .item {
  margin-left: 2em;
}

现在主要解决的问题是「标题」的定位,可以思考一下,在 flexbox 提供的现有对齐方式里,我们找不到解决方案,除非我们改造 html 代码将左侧和右侧区域分别用一个容器包裹起来,然后在父元素上 justify-content: space-between 。可以,但不优雅。

再看一遍要求:标题在 剩余可用空间 内水平 居中margin: auto 直呼我擅长。上一个例子是要求居右所以我们让某一个关键元素的 margin-left: auto 或者 margin-right: auto ,这里要求居中,那我们就给左右都 auto 均分剩余空间:

.header-panel .item.item-title {
  margin: 0 auto;
}

其实这里还有一个设置方法:给「标题」左侧的「返回」加一个 margin-right: auto ,再给「标题」右侧的「清单」加一个 margin-left: auto ,效果一样。可以,但没必要。这个问题到此也就完美解决了,下一位。

margin: auto :我要篡位

经历了上面的代码,相信大家已经对 margin: auto 这段朴实无华的代码肃然起敬,请把「膨胀」打在公屏上。 margin: auto 现在很膨胀,塔门说:诶,你 justify-content 不好使,也不要给我说什么 align-items align-self ,老夫搞布局就是一把 margin: auto 复制!粘贴!哪里不齐贴哪里!那么事实真是如此吗?我们今天有幸请到了 margin: auto 马老师本码,来为我们讲解一下,以下是来自现场的文字报道。

<ul class="flex-panel">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

.flex-panel {
  display: flex;
}

/* align-items: center */
.flex-panel > .item {
  margin: auto 0;
}

/* justify-content: center */
.flex-panel > .item:first-child {
  margin-left: auto;
}
.flex-panel > .item:last-child {
  margin-right: auto;
}

/* justify-content: space-around */
.flex-panel > .item {
  margin: 0 auto;
}

/* justify-content: space-between */
.flex-panel > .item + .item {
  margin-left: auto;
}

/* justify-content: space-evenly */
.flex-panel > .item {
  margin-left: auto;
}
.flex-panel > .item:last-child {
  margin-right: auto;
}

以上只列出了部分实现,其实所有 justify-contentalign-items 能实现的对齐效果使用 margin: auto 都可以实现,大家可以自己试一试其它效果,这里就不再赘述。但是您可能要问了:既然人家 flexbox 自带的都已经实现了这些对齐方式,这里还有必要再用 margin: auto 来实现嘛。这就是我最开始讲到的:

而且在一些特殊场景下 justify-content 的效果会有问题

来看一个场景:

底部栏空间充足

底部栏空间不足滚动

要求如下:每一块的宽度固定 width: 30% 不可伸缩,父元素剩余可用空间均分到各块之间作为间隔,当数量过多宽度超出父级容器时滚动。我们可以写出如下代码:

<style>
	.bottom-panel {
    display: flex;
    justify-content: space-evenly;
    overflow: auto;
  }
  .bottom-panel > .item {
    width: 30%;
    flex-shrink: 0;
  }
</style>

<ul class="bottom-panel">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

这段代码在子元素数量较少不会超出父级容器时表现非常完美,但是,如果子元素数量超过 3 个时,所有子元素宽度之和大于 100% 必然超出父元素,此时的效果就你太正常了,左侧的部分子元素整个或者部分会被隐藏,而且通过滚动父元素也无法令其显示,如下:

<style>
	.bottom-panel {
    display: flex;
    justify-content: space-evenly;
    overflow: auto;
  }
  .bottom-panel > .item {
    width: 30%;
    flex-shrink: 0;
  }
</style>

<ul class="bottom-panel">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

底部栏部分被隐藏

这个问题在 justify-content: center justify-content: space-around 的时候同样存在,规范里定义了一个 safe 来解决这个问题,但是很遗憾目前浏览器支持情况还非常差。

margin: auto 总是会在你最需要的时候忽然出现。

再看一下上面提到的规范定义:

Overflowing boxes ignore their auto margins and overflow in the end direction.

在这种情况下 margin: auto 会选择默默的消失,还你一份 love and peace

<style>
	.bottom-panel {
    display: flex;
    overflow: auto;
  }
  .bottom-panel > .item {
    width: 30%;
    flex-shrink: 0;
    margin-left: auto;
  }
  .bottom-panel > .item:last-child {
    margin-right: auto;
  }
</style>

<ul class="bottom-panel">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

总结

相信大家现在对 margin: auto 的认识和使用都有了更加深刻的印象,尤其和 flexbox 双剑合璧会带来很多美妙的魔法般的体验,flexbox 本身就是非常强大的布局方案,有了 margin: auto 的加持更是如虎添翼。文中如果有错误或者表达不妥的地方,欢迎大家拍砖助我进步。我本人对 CSS 非常热爱,喜欢用优雅的方式实现页面布局,也非常欢迎大家一起来探讨。

margin: auto ,永远滴神!

参考资料