Skip to content

很多 Web 开发者在还原设计师提供的设计稿时,总是喜欢给元素指定一个与设计稿模板相等的尺寸。在此过程中,有时候会忘记设计中的一些边缘情况。例如,当服务端输出的内容要比设计稿更长时,而我们又给元素指定一个与设计稿模板相等的尺寸,此时就会造成内容的溢出,甚至会打破 Web 布局。

换句话说,在设置固定尺寸的元素上改变内容时,会改变设计的外观,甚至更糟糕的是,它会破坏 Web 应用或页面的布局,使其无法访问。为了避免此现象,我们在编写 CSS 时就应该考虑这些边缘情况,比如说避免给元素指定固定尺寸,同时使其具有一定的动态性,来更好地匹配动态输出的内容,不至于 Web 布局因尺寸限制和动态输出的内容而影响整个外观或因此打破 Web 布局。

固定尺寸和长内容对 Web 布局的影响

img

正如你所看到的,Web 开发者总是能从 Web 设计师提供的设计稿中获取到元素的相关信息,比如颜色、尺寸大小、内距、字号等。这些信息对于 Web 开发者是非常重要的,因为他们在还原设计稿时,需要它们。就拿上图中的“新建帐户”按钮来说,从设计稿中可以得知:

  • 按钮的尺寸是 104px x 48px ,其中宽度是 104px ,高度是 48px
  • 按钮的背景颜色是绿色(#42b72a),文本颜色是白色(#fff);
  • 按钮内距是 16px
  • 按钮的文本字号是 18px
  • 按钮的圆角半径是 6px
  • ……

Web 开发者将会根据这些信息来还原设计稿的 UI 效果:

HTML
<button>新建帐户</button>
CSS
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;
}

img

Demo 地址:https://codepen.io/airen/full/NWLJLax

看上去一切都 OK! 事实上,并非如此!比如,你开发的 Web 应用或页面是一个多语言的应用,那么输出的内容字符串长度就会因语言版本有所不同。Web 设计师又不可能针对不同语言版本提供不同的设计稿:

img

Web 开发者不管是根据哪个语言版本来设置按钮尺寸,还原出来的 UI 效果都会不尽人意:

  • 如果设置最小值 104px ,其他语言版本就会内容溢出,甚至会打破 Web 布局;
  • 如果设置最大值 219px ,其他语言版本就有可能会有很大的空白空间。

img

Demo 地址:https://codepen.io/airen/full/OJoqooy

所以说,你在开发的过程中,除了要考虑元素的尺寸之外,还需要考虑动态内容,尤其是内容过长或过短的情况。只有这样,你开发出来的 Web 应用或页面适配性、灵活性更强。换句话说,只有这样编写的 CSS 代码才具有一定的防御性,避免因你的代码原因,给用户带来不必要的麻烦。

接下来,我们一起来讨论,在 CSS 中应该如何编写,才不会造成盒子因长内容破碎,也不会因短内容使盒子有太多空白空间存在。

容器的尺寸由谁来决定

Web 上的每一个元素都被视为一个盒子,它相当于一个容器,就好比我们生活中的器皿,通过格式化(CSS 的 display 属性的值)之后有着不同的形态:

img

不同的器皿都有着自己的大小。在 CSS 中,这些容器的大小可以是由容器的内容来决定,也可以显式地通过属性来控制。大家最为熟知的应该是 CSS 盒模型中的属性来决定一个容器(元素)的大小

只不过在指定容器大小时,你可能是明确地知道容器的确切尺寸 ,也有可能是由着容器的内容来决定尺寸大小。简单地说,就是一个是明确的尺寸 (Definite Size),一个是不确定的尺寸 (Indefinite Size)。

  • 明确的尺寸,指的是不需要执行布局就可以确定盒子的大小。也就是说,显式地给容器设置一个固定值,或内容所占区域的大小,或一个容器块的初始大小,或通过其他计算方式得到的尺寸,比如 Flexbox 布局中的“拉伸和收缩”(Stretch-fit),即 flex-growflex-shrink
  • 不确定的尺寸,指的是一个未知的尺寸。也就是说,容器的大小具备无限空间,有可能很大,也有可能很小。

通俗来说,明确的尺寸是知道元素的 width (或 inline-size)和 height (或 block-size)属性的确定值;不确定的尺寸是需要根据内容来计算的,所以要知道不确定的尺寸就需要先检查内容,也就是自动计算尺寸。

但不管是明确的尺寸还是不确定的尺寸设置,在 CSS 中都是由下面这些属性来控制:

物理属性逻辑属性(horizontal-tb)逻辑属性(vertical-lr)逻辑属性(vertical-rl)
widthinline-sizeblock-sizeblock-size
heightblock-sizeinline-sizeinline-size
min-widthmin-inline-sizemin-block-sizemin-block-size
min-heightmin-block-sizemin-inline-sizemin-inline-size
max-widthmax-inline-sizemax-block-sizemax-block-size
max-heightmax-block-sizemax-inline-sizemax-inline-size

这些属性被称为尺寸属性 ,它们可以接受 <length-percentage>autononemin-contentmax-contentfit-content (CSS Grid 中还有一个 fit-content())等值。其中 <length-percentage> 分为 <length> (就是使用长度单位的一些值,比如 100px100vw 等),<percentage> 指的是百分比单位的值,比如 50%

一般情况下,CSS 尺寸属性的值是一个 <length-percentage> 的话,则表示这个容器有一个明确的尺寸;如果 CSS 尺寸属性的值是像 automin-contentmax-contentfit-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-contentmax-contentfit-content 能根据元素内容来决定元素大小,因此它们统称为内在尺寸。
  • 外在尺寸 :元素不会考虑自身的内容,而是根据上下文来决定大小,最为典型的案例,就是 widthmin-widthmax-width 等属性使用了 % 单位的值 。

img

Demo 地址: https://codepen.io/airen/full/NWLJOrg

如何避免使用易碎的容器盒子?

现在我们已经知道了,在 Web 开发过程中,固定尺寸的盒子加上长内容容易使容器盒子破碎,即内容溢出固定尺寸的容器盒子。而且,我们也知道容器盒子的尺寸由谁来决定,结合这些内容,我们就可以知道在开发过程中如何避免使用易碎的容器盒子。主要有以下几种方式:

  • 使用 min-width/heightmax-width/height 来替代 widthheight
  • 固定尺寸 + 指示溢出(或分段溢出);
  • widthheight 上使用内在尺寸。

考虑使用 min-width 替代 width

首先,我们已经知道固定尺寸的容器(即容器指定 width 属性的值是一个固定值,比如 104px)会因内容过长打破容器。其次是,CSS 中可运用 width 的元素也可以使用 min-width 。不同的是,min-width 可以防止 width 值小于 min-width 指定的值所造成的布局缺陷 。简单地说,容器设置 min-width 可以防止长内容打破容器盒子(内容溢出)。

比如,我们把上面示例中的 width 替换成 min-width

CSS
button {
    min-width: 104px;
}

img

Demo 地址: https://codepen.io/airen/full/vYzPVrX

元素容器上设置 min-width 还有一个好处,那就是内容太短的场景能得到较好的改善。过短的内容除了可能会影响 UI 美观之外,甚至某些情况下会影响用户的操作,例如可点击区域变小,让用户点不到。或是按钮太小,让用户容易忽略它,或不觉得它是按钮。

img

Demo 地址: https://codepen.io/airen/full/VwGRgPY

不过,使用 min-width 设置容器尺寸时,最好和内距 padding 结合起来使用。要是不结合 padding 的话,容器元素内容长度大于 min-width 指定的值时,会让 UI 失去美感:

img

Demo 地址:https://codepen.io/airen/full/eYLXxMb

img

min-width 和 max-width 相结合

正如你所看到的,元素容器上设置 min-widthpadding-inline 可以使容器大小随其内容自动扩展,而且还能在较短内容时保持 UI 的美观和可交互性。看上去是完美了,似乎不会打碎容器盒子了。事实呢?它还是存有一定缺陷的,尤其是多个容器组合在一起的时候。比如卡片组件上的徽标:

CSS
.badge {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    white-space: nowrap;
    padding: 0 1.25rem 0 1rem;
    min-width: 60px;
}

img

Demo 地址: https://codepen.io/airen/full/PodLvgJ

这个时候,我们应该给 .badge 添加 max-width 属性,对其宽度做一定的约束。仅仅这样做是不够的,因为内容过长的时候,内容还是会引起溢出问题,所以还需要考虑对溢出内容做相应的处理,比如使用指示溢出,比如多行显示等。

CSS
.badge {
    max-width: 120px;
    padding: 0 1.25rem 0 1rem;
    min-width: 60px;
}

.badge span {
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}

img

Demo 地址: https://codepen.io/airen/full/abaxmyE

注意,Flexbox 和 Grid 布局中,不能直接对它们的匿名项目上使用指示溢出,需要给匿名项目之外添加一个容器,有关于这部分的详细介绍,可以阅读《溢出常见问题与排查》。

这种处理模式虽然粗暴有效,它的不好之处是信息可能会丢失,所以需要通过其他的方法让用户看到隐藏起来的信息,比如鼠标悬浮(:hover)在卡片(.card)上时,改变 .badgemax-width 值,把隐藏的信息透露给用户。

除了使用指示溢出之外,更好的方案是,直接让文本断行。这样一来,只需要在 .badge 上设置 max-width 即可:

CSS
.badge {
    min-width: 60px;
    max-width: 120px;
}

当然,你可以在这个基础上做得更好一些,比如在 .badge 上使用 word-break: break-word 防止长字符串或非法字符串溢出容器:

CSS
.badge {
    min-width: 60px;
    max-width: 120px;
    word-break: break-word;
}

你也可以在断行的基础上使用分段溢出,即限制行数:

CSS
.badge {
    min-width: 60px;
    max-width: 120px;
    display: -webkit-box;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

img

Demo 地址: https://codepen.io/airen/full/NWLmRBx

注意,这里有一个小技巧,如果你希望在徽标组件(.badge)中限制它每行显示的字数,那么你可以考虑使用 CSS 的 ch 单位。ch 单位等于元素字体中 0 字符宽度

img

需要注意的是 ch 较为适合英文,如果针对中文的话,建议使用 em 单位(一个 em 就是一个中文汉字的宽度)。

考虑使用 min-height 替代 height

同样的,在 Web 开发过程中需要给一个容器设置高度(height)时,也应该像设置宽度(width)一样,使用 min-height 或者和 max-height 相结合来替代 height

CSS
.card {
    min-height: 150px;
}

img

Demo 地址: https://codepen.io/airen/full/MWqRbYr

在某些情况下,我们面临的场景是不知道内容的高度,比如手风琴、下拉菜单等。特别是制作一些动效的情景之下,使用 max-height 可能是一个很好的解决方案。比如下面这个手风琴的示例:

CSS
p {
    max-height: 800px;
    opacity: 1;
}

input[type=checkbox]:checked ~ p {
    max-height: 0;
    opacity: 0;
}

img

Demo 地址: https://codepen.io/airen/full/PodgbKb

max-heightmax-width 还有一个很有意思的案例。你可以借助视窗单位 vwmax-width 以及 max-height 实现一种流式响应式宽高比效果,即模拟 aspect-ratio 属性的效果。

img

假设你有一张尺寸为 644px x 1000px 的图片。如果要实现流式的宽高比(响应式),我们需要:

  • 高宽比:height/width
  • 容器的宽度:可以是固定的数值,也可以是流式的(比如 100%100vw) ;
  • 设置 height 为视窗高度乘以宽高比 ;
  • 设置 max-height,即容器宽度乘以宽高比;
  • 设置 max-width 为容器宽度 。

如果用代码来表述的话,如下所示:

CSS
 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); 
 }

调整视窗大小,你可以看到效果如下:

img

Demo 地址:https://codepen.io/airen/full/vYzMyRL

使用 CSS 的比较函数

min-widthmax-widthmin-heightmax-height 它们可以对容器盒子的宽高做相应的约束,其次还有着共同的一个特点,它们都是 CSS 的属性。另外,它们的值最终是和 CSS 的 widthheight 的计算值进行比较,然后选择符合条件的值。其实,CSS 的比较函数 min()max()clamp() 用于 widthheight 时,有着与它们相似的功能。

我们可以使用min()函数给元素设置最大值。比如:

CSS
.element {
    width: min(50vw, 500px);
    
    /* 等同于 */
    width: 50vw;
    max-width: 500px;
}

同样的,我们可以使用 max() 函数给元素设置最小值

CSS
.element {
    width: max(50vw, 500px);
    
    /* 等同于 */
    width: 50vw;
    min-width: 500px;
}

clamp() 函数要比 min()max() 函数要复杂一点,它返回的是一个区间值。clamp() 函数接受三个参数,即 clamp(MIN, VAL, MAX),其中:

  • MIN 表示最小值;
  • VAL 表示首选值;
  • MAX 表示最大值。

它们之间:

  • 如果 VALMINMAX 之间,则使用 VAL 作为函数的返回值;
  • 如果 VAL 大于 MAX ,则使用 MAX 作为函数的返回值;
  • 如果 VAL 小于 MIN ,则使用 MIN 作为函数的返回值。
CSS
.element {
    width: clamp(100px, 50vw, 500px);
}

就该示例而言,clamp(100px, 50vw, 500px) 可以这样来理解:

  • 元素 .element 的宽度不会小于 100px(有点类似于元素设置了 min-width: 100px);
  • 元素 .element 的宽度不会大于 500px(有点类似于元素设置了 max-width: 500px);
  • 首选值 VAL50vw ,只有当浏览器视窗的宽度大于 200px 且小于 1000px 时才会有效,即元素 .element 的宽度为 50vw(有点类似于元素设置了 width:50vw)。

它相当于使用了 min()max() 函数,具体地说:

CSS
clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX))

就上面示例而言:

CSS
.element { 
    width: clamp(100px, 50vw, 500px);
    
    /* 等同于 */
    width: max(100px, min(50vw, 500px));
 }

正如前面所述,clamp() 的计算会经历以下几个步骤:

CSS
.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() 函数中的参数,计算的值取决于上下文

正如你所见,如果在 widthheight 上使用 min()max()clamp() 函数时,可以让容器有一个动态值。尤其是 clamp() ,可以让容器的尺寸加把锁。例如:

CSS
: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

img

Demo 地址:https://codepen.io/airen/full/YzOMZxR

使用内在尺寸

如果你有接触过 内在 Web 设计 的话,那么就知道,设置容器尺寸的时候,可以在 widthheight 上使用内在尺寸相关的属性值,比如 min-contentmax-contentfit-content

img

声明,上图来自于 @stefanjudis 录制了一个视频

来看一个简单示例,分别给 h1width 设置了automin-contentmax-contentfit-content

CSS
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;
}

img

Demo 地址: https://codepen.io/airen/full/XWPQMEe

正如你所看到的,容器元素 h1 会根据其内在的内容来决定自己的宽度。其中 automin-contentmax-content 相比较 fit-content 更易于理解一些。因为, fit-content 会检查可用空间(fill-available)与 max-contentmin-content 大小,最后决定 width 取值:

img

另外,从本质上讲,fit-content 是以下内容的简写模式:

CSS
.box {
    width: fit-content;
}

/* 等同于 */
.box {
    width: auto;
    min-width: min-content;
    max-width: max-content;
}

也就是说,在 CSS 中,我们现在有两种设置大小的方式:内部尺寸设置外部尺寸设置 。后者是最常见的,这意味着使用固定的元素宽度或高度值。这样做有一个极大的缺陷,内容断行或内容溢出

img

很多时候我们并不知道容器的内容会是什么,所占宽度是多少,这就会造成上图的现象。所以我们设置元素尺寸大小时,使用 min-contentmax-contentfit-content 就可以让元素的大小取决于它的内容大小,避免内容溢出或断行等现象。

来看一个这方面的示例,带有标题的图片示例,标题的宽度是其父元素宽度的 100% (默认情况之下),因为标题它自身就是一个块元素:

HTML
<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 即可:

CSS
figure {
    width: max-content;
}

figure img {
    display: block;
    max-width: 100%;
    height: auto;
    object-fit: cover;
}

最终渲染出来的结果和我们所期待的是有所差异的。

通过前面的内容,可以知道,当 figurewidth 设置为 max-content 时,它的宽度等于其内容:

  • img 的原始宽度大于图片标题(figcaption)时,将会以 img 的原始宽度作为 figure 的宽度;
  • img 的原始宽度小于图片标题(figcaption)时,将会以 figcaption 的内容宽度作为 figure 的宽度。

只有当 img 的原始宽度和 figcaption 内容宽度相等之时,才能实现图片和标题一样宽度的效果。要实现这个效果,需要将 figurewidth 设置为 fit-content

CSS
figure {
    width: fit-content;
    
    /* 它等同于 */
    width: auto;
    min-width: min-content;
    max-width: max-content;
}

如此一来,如果图像的宽度非常大,它也不会超过视窗宽度:

img

Demo 地址:https://codepen.io/airen/full/jOvRBoB

注意,min-contentmax-contentfit-content 除了可以用于 widthheight 之外,还可以用于 Flexbox 的 flex-basis 属性上以及用来定义网格轨道尺寸。有关于这部分的内容介绍已超出这节课的范畴,如果感兴趣的话,可以阅读 现代 Web 布局中的《内在 Wed 设计》和《固定网格轨道尺寸给 Web 布局带来的局限性》。

特别声明,课程中的内容都是围绕着 CSS 的物理属性展开的,其实上面所介绍的内容也同样适用于 CSS 相对应的逻辑属性中,比如:

  • 考虑使用 min-inline-size 替代 inline-size
  • min-inline-sizemax-inline-size 相结合;
  • 考虑使用 min-block-size 替代 block-size
  • min-block-sizemax-block-size 相结合;
  • min()max()clamp() 函数也可以用于 inline-sizeblock-size
  • min-contentmax-contentfit-content 也可以用于 inline-sizeblock-size

小结

在 Web 开发过程中,如果给容器盒子设置一个固定尺寸,很容易因太长的内容破碎(打破 Web 布局),也很容易因太短的内容影响 UI 外观。庆幸的是,我们可以有很多手段避免这些现象出现:

  • 使用 min-*max-* 相结合,只对容器盒子尺寸做一定的约束;
  • 使用断行、指示溢出和分段溢出等手段,避免内容溢出容器;
  • 使用比较函数和内在尺寸来定义容器盒子尺寸。