很多 Web 开发者在还原设计师提供的设计稿时,总是喜欢给元素指定一个与设计稿模板相等的尺寸。在此过程中,有时候会忘记设计中的一些边缘情况。例如,当服务端输出的内容要比设计稿更长时,而我们又给元素指定一个与设计稿模板相等的尺寸,此时就会造成内容的溢出,甚至会打破 Web 布局。
换句话说,在设置固定尺寸的元素上改变内容时,会改变设计的外观,甚至更糟糕的是,它会破坏 Web 应用或页面的布局,使其无法访问。为了避免此现象,我们在编写 CSS 时就应该考虑这些边缘情况,比如说避免给元素指定固定尺寸,同时使其具有一定的动态性,来更好地匹配动态输出的内容,不至于 Web 布局因尺寸限制和动态输出的内容而影响整个外观或因此打破 Web 布局。
固定尺寸和长内容对 Web 布局的影响
正如你所看到的,Web 开发者总是能从 Web 设计师提供的设计稿中获取到元素的相关信息,比如颜色、尺寸大小、内距、字号等。这些信息对于 Web 开发者是非常重要的,因为他们在还原设计稿时,需要它们。就拿上图中的“新建帐户”按钮来说,从设计稿中可以得知:
- 按钮的尺寸是
104px x 48px
,其中宽度是104px
,高度是48px
; - 按钮的背景颜色是绿色(
#42b72a
),文本颜色是白色(#fff
); - 按钮内距是
16px
; - 按钮的文本字号是
18px
; - 按钮的圆角半径是
6px
; - ……
Web 开发者将会根据这些信息来还原设计稿的 UI 效果:
<button>新建帐户</button>
button {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: #42b72a;
color: #fff;
width: 104px;
height: 48px;
border: none 0;
padding: 0 16px;
border-radius: 6px;
font-size: 18px;
box-sizing: border-box;
}
看上去一切都 OK! 事实上,并非如此!比如,你开发的 Web 应用或页面是一个多语言的应用,那么输出的内容字符串长度就会因语言版本有所不同。Web 设计师又不可能针对不同语言版本提供不同的设计稿:
Web 开发者不管是根据哪个语言版本来设置按钮尺寸,还原出来的 UI 效果都会不尽人意:
- 如果设置最小值
104px
,其他语言版本就会内容溢出,甚至会打破 Web 布局; - 如果设置最大值
219px
,其他语言版本就有可能会有很大的空白空间。
所以说,你在开发的过程中,除了要考虑元素的尺寸之外,还需要考虑动态内容,尤其是内容过长或过短的情况。只有这样,你开发出来的 Web 应用或页面适配性、灵活性更强。换句话说,只有这样编写的 CSS 代码才具有一定的防御性,避免因你的代码原因,给用户带来不必要的麻烦。
接下来,我们一起来讨论,在 CSS 中应该如何编写,才不会造成盒子因长内容破碎,也不会因短内容使盒子有太多空白空间存在。
容器的尺寸由谁来决定
Web 上的每一个元素都被视为一个盒子,它相当于一个容器,就好比我们生活中的器皿,通过格式化(CSS 的 display
属性的值)之后有着不同的形态:
不同的器皿都有着自己的大小。在 CSS 中,这些容器的大小可以是由容器的内容来决定,也可以显式地通过属性来控制。大家最为熟知的应该是 CSS 盒模型中的属性来决定一个容器(元素)的大小 。
只不过在指定容器大小时,你可能是明确地知道容器的确切尺寸 ,也有可能是由着容器的内容来决定尺寸大小。简单地说,就是一个是明确的尺寸 (Definite Size),一个是不确定的尺寸 (Indefinite Size)。
- 明确的尺寸,指的是不需要执行布局就可以确定盒子的大小。也就是说,显式地给容器设置一个固定值,或内容所占区域的大小,或一个容器块的初始大小,或通过其他计算方式得到的尺寸,比如 Flexbox 布局中的“拉伸和收缩”(Stretch-fit),即
flex-grow
和flex-shrink
。 - 不确定的尺寸,指的是一个未知的尺寸。也就是说,容器的大小具备无限空间,有可能很大,也有可能很小。
通俗来说,明确的尺寸是知道元素的 width
(或 inline-size
)和 height
(或 block-size
)属性的确定值;不确定的尺寸是需要根据内容来计算的,所以要知道不确定的尺寸就需要先检查内容,也就是自动计算尺寸。
但不管是明确的尺寸还是不确定的尺寸设置,在 CSS 中都是由下面这些属性来控制:
物理属性 | 逻辑属性(horizontal-tb) | 逻辑属性(vertical-lr) | 逻辑属性(vertical-rl) |
---|---|---|---|
width | inline-size | block-size | block-size |
height | block-size | inline-size | inline-size |
min-width | min-inline-size | min-block-size | min-block-size |
min-height | min-block-size | min-inline-size | min-inline-size |
max-width | max-inline-size | max-block-size | max-block-size |
max-height | max-block-size | max-inline-size | max-inline-size |
这些属性被称为尺寸属性 ,它们可以接受 <length-percentage>
、auto
、none
、min-content
、max-content
和 fit-content
(CSS Grid 中还有一个 fit-content()
)等值。其中 <length-percentage>
分为 <length>
(就是使用长度单位的一些值,比如 100px
、100vw
等),<percentage>
指的是百分比单位的值,比如 50%
。
一般情况下,CSS 尺寸属性的值是一个 <length-percentage>
的话,则表示这个容器有一个明确的尺寸;如果 CSS 尺寸属性的值是像 auto
、min-content
、max-content
和 fit-content
的话,则表示这个容器的尺寸是不明确的,需要根据内容来计算。
换句话说,在 CSS 中,任何一个容器都有四种自动计算尺寸大小的方式:
auto
:会根据格式化上下文自动计算容器的尺寸;min-content
:是在不导致溢出的情况下,容器的内容的最小尺寸;max-content
:容器可以容纳的最大尺寸,如果容器中包含未格式化的文本,那么它将显示为一个完整的长字符串;fit-content
:如果给定轴中的可用空间是确定的,则等于min(max-content, max(min-content, stretch-fit))
,反之则等于max-content
。
它们在 CSS 中又被称为 “内在尺寸(Intrinsic Size) ”和“外在尺寸(Extrinsic Size) ”:
- 内在尺寸 :元素根据自身的内容(包括其后代元素)决定大小,而不需要考虑其上下文,其中
min-content
、max-content
和fit-content
能根据元素内容来决定元素大小,因此它们统称为内在尺寸。 - 外在尺寸 :元素不会考虑自身的内容,而是根据上下文来决定大小,最为典型的案例,就是
width
、min-width
、max-width
等属性使用了%
单位的值 。
如何避免使用易碎的容器盒子?
现在我们已经知道了,在 Web 开发过程中,固定尺寸的盒子加上长内容容易使容器盒子破碎,即内容溢出固定尺寸的容器盒子。而且,我们也知道容器盒子的尺寸由谁来决定,结合这些内容,我们就可以知道在开发过程中如何避免使用易碎的容器盒子。主要有以下几种方式:
- 使用
min-width/height
和max-width/height
来替代width
和height
; - 固定尺寸 + 指示溢出(或分段溢出);
- 在
width
或height
上使用内在尺寸。
考虑使用 min-width 替代 width
首先,我们已经知道固定尺寸的容器(即容器指定 width
属性的值是一个固定值,比如 104px
)会因内容过长打破容器。其次是,CSS 中可运用 width
的元素也可以使用 min-width
。不同的是,min-width
可以防止 width
值小于 min-width
指定的值所造成的布局缺陷 。简单地说,容器设置 min-width
可以防止长内容打破容器盒子(内容溢出)。
比如,我们把上面示例中的 width
替换成 min-width
:
button {
min-width: 104px;
}
元素容器上设置 min-width
还有一个好处,那就是内容太短的场景能得到较好的改善。过短的内容除了可能会影响 UI 美观之外,甚至某些情况下会影响用户的操作,例如可点击区域变小,让用户点不到。或是按钮太小,让用户容易忽略它,或不觉得它是按钮。
不过,使用 min-width
设置容器尺寸时,最好和内距 padding
结合起来使用。要是不结合 padding
的话,容器元素内容长度大于 min-width
指定的值时,会让 UI 失去美感:
min-width 和 max-width 相结合
正如你所看到的,元素容器上设置 min-width
和 padding-inline
可以使容器大小随其内容自动扩展,而且还能在较短内容时保持 UI 的美观和可交互性。看上去是完美了,似乎不会打碎容器盒子了。事实呢?它还是存有一定缺陷的,尤其是多个容器组合在一起的时候。比如卡片组件上的徽标:
.badge {
display: inline-flex;
justify-content: center;
align-items: center;
white-space: nowrap;
padding: 0 1.25rem 0 1rem;
min-width: 60px;
}
这个时候,我们应该给 .badge
添加 max-width
属性,对其宽度做一定的约束。仅仅这样做是不够的,因为内容过长的时候,内容还是会引起溢出问题,所以还需要考虑对溢出内容做相应的处理,比如使用指示溢出,比如多行显示等。
.badge {
max-width: 120px;
padding: 0 1.25rem 0 1rem;
min-width: 60px;
}
.badge span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
注意,Flexbox 和 Grid 布局中,不能直接对它们的匿名项目上使用指示溢出,需要给匿名项目之外添加一个容器,有关于这部分的详细介绍,可以阅读《溢出常见问题与排查》。
这种处理模式虽然粗暴有效,它的不好之处是信息可能会丢失,所以需要通过其他的方法让用户看到隐藏起来的信息,比如鼠标悬浮(:hover
)在卡片(.card
)上时,改变 .badge
的 max-width
值,把隐藏的信息透露给用户。
除了使用指示溢出之外,更好的方案是,直接让文本断行。这样一来,只需要在 .badge
上设置 max-width
即可:
.badge {
min-width: 60px;
max-width: 120px;
}
当然,你可以在这个基础上做得更好一些,比如在 .badge
上使用 word-break: break-word
防止长字符串或非法字符串溢出容器:
.badge {
min-width: 60px;
max-width: 120px;
word-break: break-word;
}
你也可以在断行的基础上使用分段溢出,即限制行数:
.badge {
min-width: 60px;
max-width: 120px;
display: -webkit-box;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
注意,这里有一个小技巧,如果你希望在徽标组件(.badge
)中限制它每行显示的字数,那么你可以考虑使用 CSS 的 ch
单位。ch
单位等于元素字体中 0
字符宽度 。
需要注意的是 ch
较为适合英文,如果针对中文的话,建议使用 em
单位(一个 em
就是一个中文汉字的宽度)。
考虑使用 min-height 替代 height
同样的,在 Web 开发过程中需要给一个容器设置高度(height
)时,也应该像设置宽度(width
)一样,使用 min-height
或者和 max-height
相结合来替代 height
。
.card {
min-height: 150px;
}
在某些情况下,我们面临的场景是不知道内容的高度,比如手风琴、下拉菜单等。特别是制作一些动效的情景之下,使用 max-height
可能是一个很好的解决方案。比如下面这个手风琴的示例:
p {
max-height: 800px;
opacity: 1;
}
input[type=checkbox]:checked ~ p {
max-height: 0;
opacity: 0;
}
max-height
和 max-width
还有一个很有意思的案例。你可以借助视窗单位 vw
和 max-width
以及 max-height
实现一种流式响应式宽高比效果,即模拟 aspect-ratio
属性的效果。
假设你有一张尺寸为 644px x 1000px
的图片。如果要实现流式的宽高比(响应式),我们需要:
- 高宽比:
height/width
; - 容器的宽度:可以是固定的数值,也可以是流式的(比如
100%
或100vw
) ; - 设置
height
为视窗高度乘以宽高比 ; - 设置
max-height
,即容器宽度乘以宽高比; - 设置
max-width
为容器宽度 。
如果用代码来表述的话,如下所示:
img {
--ratio: 664 / 1000;
--container-width: 30em;
display: block;
height: calc(100vw * var(--ratio));
max-height: calc(var(--container-width) * var(--ratio));
width: 100%;
max-width: var(--container-width);
}
调整视窗大小,你可以看到效果如下:
使用 CSS 的比较函数
min-width
、max-width
、min-height
和 max-height
它们可以对容器盒子的宽高做相应的约束,其次还有着共同的一个特点,它们都是 CSS 的属性。另外,它们的值最终是和 CSS 的 width
或 height
的计算值进行比较,然后选择符合条件的值。其实,CSS 的比较函数 min()
、max()
和 clamp()
用于 width
和 height
时,有着与它们相似的功能。
我们可以使用min()
函数给元素设置最大值。比如:
.element {
width: min(50vw, 500px);
/* 等同于 */
width: 50vw;
max-width: 500px;
}
同样的,我们可以使用 max()
函数给元素设置最小值:
.element {
width: max(50vw, 500px);
/* 等同于 */
width: 50vw;
min-width: 500px;
}
clamp()
函数要比 min()
和 max()
函数要复杂一点,它返回的是一个区间值。clamp()
函数接受三个参数,即 clamp(MIN, VAL, MAX)
,其中:
MIN
表示最小值;VAL
表示首选值;MAX
表示最大值。
它们之间:
- 如果
VAL
在MIN
和MAX
之间,则使用VAL
作为函数的返回值; - 如果
VAL
大于MAX
,则使用MAX
作为函数的返回值; - 如果
VAL
小于MIN
,则使用MIN
作为函数的返回值。
.element {
width: clamp(100px, 50vw, 500px);
}
就该示例而言,clamp(100px, 50vw, 500px)
可以这样来理解:
- 元素
.element
的宽度不会小于100px
(有点类似于元素设置了min-width: 100px
); - 元素
.element
的宽度不会大于500px
(有点类似于元素设置了max-width: 500px
); - 首选值
VAL
为50vw
,只有当浏览器视窗的宽度大于200px
且小于1000px
时才会有效,即元素.element
的宽度为50vw
(有点类似于元素设置了width:50vw
)。
它相当于使用了 min()
和 max()
函数,具体地说:
clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX))
就上面示例而言:
.element {
width: clamp(100px, 50vw, 500px);
/* 等同于 */
width: max(100px, min(50vw, 500px));
}
正如前面所述,clamp()
的计算会经历以下几个步骤:
.element {
width: clamp(100px, 50vw, 500px);
/* 50vw 相当于视窗宽度的一半,如果视窗宽度是 760px 的话,那么 50vw 相当等于 380px */
width: clamp(100px, 380px, 500px);
/* 用 min() 和 max() 描述*/
width: max(100px, min(380px, 500px));
/* min(380px, 500px) 返回的值是 380px */
width: max(100px, 380px);
/* max(100px, 380px) 返回的值是 380px */
width: 380px;
}
这和前面介绍 clamp(MIN,VAL,MAX)
时计算出来结果一致(VAL
大于 MIN
且小于 MAX
会取首选值 VAL
)。
你有没有发现,我们在 min()
、max()
和 clamp()
函数参数使用 vw
时,最终的计算值是依据视窗的宽度来计算的。换句话说,我们在使用这些函数时,参数中运用的不同值单位对最终值是有一定影响的,也就是说: min()
、max()
和 clamp()
函数中的参数,计算的值取决于上下文 。
正如你所见,如果在 width
和 height
上使用 min()
、max()
和 clamp()
函数时,可以让容器有一个动态值。尤其是 clamp()
,可以让容器的尺寸加把锁。例如:
:root {
/* 理想视窗宽度,就是设计稿宽度 */
--ideal-viewport-width: 1600;
/* 当前视窗宽度 100vw */
--current-viewport-width: 100vw;
/* 最小视窗宽度 */
--min-viewport-wdith: 320px;
/* 最大视窗宽度 */
--max-viewport-width: 3840px;
/**
* clamp() 接受三个参数值,MIN、VAL 和 MAX,即 clamp(MIN, VAL, MAX)
* MIN:最小值,对应的是最小视窗宽度,即 --min-viewport-width
* VAL:首选值,对应的是100vw,即 --current-viewport-width
* MAX:最大值,对应的是最大视窗宽度,即 --max-viewport-width
**/
--clamped-viewport-width: clamp( var(--min-viewport-width), var(--current-viewport-width), var(--max-viewport-width) )
}
.card {
/* 理想元素尺寸基数 */
--ideal-card-width: 690;
/* 1600px 设计稿中卡片宽度,理想宽度 */
width: calc( var(--ideal-card-width) * var(--clamped-viewport-width) / var(--ideal-viewport-width) )
}
在长度相关的属性使用该特性,还可以帮助我们构建出响应式 UI:
使用内在尺寸
如果你有接触过 内在 Web 设计 的话,那么就知道,设置容器尺寸的时候,可以在 width
和 height
上使用内在尺寸相关的属性值,比如 min-content
、max-content
和 fit-content
。
声明,上图来自于 @stefanjudis 录制了一个视频。
来看一个简单示例,分别给 h1
的 width
设置了auto
、min-content
、max-content
和 fit-content
:
h1[data-width="auto"] {
width: auto;
}
h1[data-width="min-content"] {
width: min-content;
}
h1[data-width="max-content"] {
width: max-content;
}
h1[data-width="fit-content"] {
width: fit-content;
}
正如你所看到的,容器元素 h1
会根据其内在的内容来决定自己的宽度。其中 auto
、min-content
和 max-content
相比较 fit-content
更易于理解一些。因为, fit-content
会检查可用空间(fill-available
)与 max-content
和 min-content
大小,最后决定 width
取值:
另外,从本质上讲,fit-content
是以下内容的简写模式:
.box {
width: fit-content;
}
/* 等同于 */
.box {
width: auto;
min-width: min-content;
max-width: max-content;
}
也就是说,在 CSS 中,我们现在有两种设置大小的方式:内部尺寸设置 和 外部尺寸设置 。后者是最常见的,这意味着使用固定的元素宽度或高度值。这样做有一个极大的缺陷,内容断行或内容溢出 :
很多时候我们并不知道容器的内容会是什么,所占宽度是多少,这就会造成上图的现象。所以我们设置元素尺寸大小时,使用 min-content
、max-content
和 fit-content
就可以让元素的大小取决于它的内容大小,避免内容溢出或断行等现象。
来看一个这方面的示例,带有标题的图片示例,标题的宽度是其父元素宽度的 100%
(默认情况之下),因为标题它自身就是一个块元素:
<figure>
<img src="thumbnail.jpg" alt="" />
<figcaption>
<p>欢迎来到小册,该小册全面介绍现代 Web 布局技术。这一节主要介绍的是内在 Web 布局,我们将详细介绍内在 Web 布局将会使用到的 CSS 技术,比如 min-content、max-content、 fit-content 等特性的使用 ...</p>
</figcaption>
</figure>
如果你需要让图片宽度和标题宽度一样宽,原本以为在 figure
上设置 width
的值为 max-content
即可:
figure {
width: max-content;
}
figure img {
display: block;
max-width: 100%;
height: auto;
object-fit: cover;
}
最终渲染出来的结果和我们所期待的是有所差异的。
通过前面的内容,可以知道,当 figure
的 width
设置为 max-content
时,它的宽度等于其内容:
- 当
img
的原始宽度大于图片标题(figcaption
)时,将会以img
的原始宽度作为figure
的宽度; - 当
img
的原始宽度小于图片标题(figcaption
)时,将会以figcaption
的内容宽度作为figure
的宽度。
只有当 img
的原始宽度和 figcaption
内容宽度相等之时,才能实现图片和标题一样宽度的效果。要实现这个效果,需要将 figure
的 width
设置为 fit-content
:
figure {
width: fit-content;
/* 它等同于 */
width: auto;
min-width: min-content;
max-width: max-content;
}
如此一来,如果图像的宽度非常大,它也不会超过视窗宽度:
注意,min-content
、max-content
和 fit-content
除了可以用于 width
和 height
之外,还可以用于 Flexbox 的 flex-basis
属性上以及用来定义网格轨道尺寸。有关于这部分的内容介绍已超出这节课的范畴,如果感兴趣的话,可以阅读 现代 Web 布局中的《内在 Wed 设计》和《固定网格轨道尺寸给 Web 布局带来的局限性》。
特别声明,课程中的内容都是围绕着 CSS 的物理属性展开的,其实上面所介绍的内容也同样适用于 CSS 相对应的逻辑属性中,比如:
- 考虑使用
min-inline-size
替代inline-size
; min-inline-size
和max-inline-size
相结合;- 考虑使用
min-block-size
替代block-size
; min-block-size
和max-block-size
相结合;min()
、max()
和clamp()
函数也可以用于inline-size
和block-size
;min-content
、max-content
和fit-content
也可以用于inline-size
和block-size
。
小结
在 Web 开发过程中,如果给容器盒子设置一个固定尺寸,很容易因太长的内容破碎(打破 Web 布局),也很容易因太短的内容影响 UI 外观。庆幸的是,我们可以有很多手段避免这些现象出现:
- 使用
min-*
和max-*
相结合,只对容器盒子尺寸做一定的约束; - 使用断行、指示溢出和分段溢出等手段,避免内容溢出容器;
- 使用比较函数和内在尺寸来定义容器盒子尺寸。