图解CSS:Grid布局案例之构建 Full-Bleed 布局
Full-Bleed 是印刷界中的一个概念,被称为 全出血,即在印刷中,我们有出血量,这是纸张被修剪的地方以外的区域。正因如此,印刷设计师习惯于在设计工作中考虑出血量。我们通过设置安全区域来做到这一点。
这几年,这种被称为“全出血”的概念也运用到 Web 的布局中。就是在受限宽度的一列中使用全宽元素的布局,比如在较窄的一列文本中使用一个边缘到边缘的图像:
在社区中,也有人把这种布局效果称为 Full-Width 布局,也有人称为 Edge-To-Edge 布局。说实话,在Web中实现这种布局效果,已不是难事,社区中有很多种不同的技术方案,都可以达到这个布局效果。不过,今天我们以不同的角度来思考这个问题!
我们的目标
我们今天最大目标是 在限定宽度的容器中实现全屏布局效果。别的咱先别说,先从设计的角度来看:
在限制容器内放置内容,从而提高页面内容可读性。但有时,可能会在 Web 中出现某个图片需要全屏效果。简言之,就是在某限制宽度的容器内实现全屏的效果。
比如下图的效果:
我们可以看到,我们真正追求的并不是一个有边距的固定宽度的容器。它看上去更像是一个三栏式的布局,一个主栏的两侧有两个沟槽。大多数的内容将只填充中间部分(主栏),但我们的超级花哨的视觉内容(比如图像)将横跨所有三栏。
简单地分析一下 Full-Bleed
前面我们已经说了,Full-Bleed 布局,看上去就是一个三栏布局,只不过他有着自己的特色:
- 主栏有一个固定宽度,比如
65ch
,而且水平居中,即距离视窗两侧边缘距离相等,相当于margin-left
和margin-right
值为auto
- 两个侧栏相等,即可主栏距离视窗两侧边缘相等,相当于
margin-left
和margin-right
值为auto
,也是(100vw - 主栏宽度) / 2
,即calc((100vw - 65ch) / 2)
- 主栏和两个侧栏之间有一个沟槽,比如
10px
- 窄屏的时候,两个侧边栏看不到,主要主栏和两侧的沟槽
- 全屏则横跨三栏,相当于
100vw
构建 Full-Bleed 布局
现在对 Full-Bleed 布局有了一个基本的了解,接下来一步一步来看如何实现这种布局效果。先从最老的布局技术开始。
容器分层
在还没有现代布局技术的时候,要实现Full-Bleed布局效果,主要还是在容器层上做区分处理。即用结构和类名来区分:
<!-- HTML -->
<section><!-- 默认状态 -->
<div class="container">
<!-- 限定宽度,且水平居中 -->
</div>
</section>
<section class="full__bleed"><!-- 全屏状态,使用 full__bleed 类名,区分默认状态 -->
<div class="container">
<!-- 全屏状态 -->
</div>
</section>
从上面的 HTML 结构中不难发现,采用不同的容器和类名来区分全屏和限宽两种状态。我们可以这样来写 CSS:
section {
display: flex;
justify-content: center;
}
.container {
width: 100%;
max-width: 65ch; /* 限定宽度*/
padding-left: 10px;
padding-right: 10px;
/* 如果 section 不用 Flexbox 可以设置 margin-left 和 margin-right 为 auto,让其水平居中 */
}
/* 全屏状态,重置 container 容器样式 */
.full__bleed .container {
max-width: none;
padding-left: 0;
padding-right: 0;
}
调整视窗大小,效果如下:
就这个方案而言,我们调整的是固定宽度和沟槽的值,如此一来,我们可以借助 CSS 自定义属性,让Full-Bleed布局变得更灵活:
:root {
--limit-container-width: 65ch;
--gutter: 1rem;
}
section {
display: flex;
justify-content: center;
}
.container {
width: 100%;
max-width: var(--limit-container-width); /* 限定宽度*/
padding-left: var(--gutter);
padding-right: var(--gutter);
/* 如果 section 不用 Flexbox 可以设置 margin-left 和 margin-right 为 auto,让其水平居中 */
}
/* 全屏状态,重置 container 容器样式 */
.full__bleed .container {
--gutter: 0;
--limit-container-width: none;
}
如果你对 CSS 自定义属性有过了解,那么我们还可以借助行内样式调整自定义属性值特性,让Full-Bleed变得更灵活。简单地说,在全屏的状态下重置自定义属性值:
<!-- HTML -->
<section><!-- 默认状态 -->
<div class="container">
<!-- 限定宽度,且水平居中 -->
</div>
</section>
<section class="full__bleed"><!-- 全屏状态,使用 full__bleed 类名,区分默认状态 -->
<div class="container" style="--limit-container-width: none;--gutter: 0;"><!-- 行内重置自定义属性值 -->
<!-- 全屏状态 -->
</div>
</section>
/* CSS */
section {
display: flex;
justify-content: center;
}
.container {
--limit-container-width: 65ch;
--gutter: 1rem;
width: 100%;
max-width: var(--limit-container-width); /* 限定宽度*/
padding-left: var(--gutter);
padding-right: var(--gutter);
/* 如果 section 不用 Flexbox 可以设置 margin-left 和 margin-right 为 auto,让其水平居中 */
}
calc() + 100vw
calc()
配合视窗全屏宽度100vw
,也可以实现 Full-Bleed 布局效果。采用这种方式,可以让我们的 HTML 结构变得简单一些,可以不用像上面的示例那相分层:
<!-- HTML -->
<div class="container">
<h1>Page Title</h1>
<p>Des ...</p>
<div class="full__bleed">
<img src="" alt="" />
</div>
<!-- 其他内空 -->
<div class="content">....</div>
</div>
我们可以在最外的容器 .container
使用 max-width
和 margin-left
以及 margin-right
为 auto
让其水平居中:
.container {
--limit-container-width: 65ch;
--gutter: 1rem;
width: 100%;
max-width: var(--limit-container-width); /* 限定宽度*/
padding-left: var(--gutter);
padding-right: var(--gutter);
margin-left: auto;
margin-right: auto;
}
简单地分析一下,容器.container
的max-width
是65ch
(宽屏下),窄屏是 width: 100%
(相当于视窗全屏)。他们之间相差就是 65ch - 100vw
,但我们只需要向左拉出其值的一半,也就是 (65ch - 100vw) / 2
。在这个基础上,还可以进行优化。 65ch
相当于 100%
,这样一来 .full__bleed
就可以是:
.full__bleed {
width: 100vw;
margin-left: calc(50% - 50vw);
}
这里的关键是,不能把 .full__bleed
的 width
设置为 100%
,因为百分比计算是相对于其父容器 .container
,也就是说 .full__bleed
和 .container
等宽,但它实际要的宽度是 100vw
。简单地说,.full__bleed
设置width: 100%
将会使其失去作用。
不过,父元素的宽度(该示例是 max-width
)是有用的,因为 .full__bleed
的 margin-left
为 50%
,告诉浏览器,将元素左边缘对准其父元素的中心,这样做,将左边缘设置为父元素宽度一半。最后,使用 calc()
从50%
的边距中减去50vw
。记住,50vw
等同于视窗宽度的一半(100vw / 2
)。这就是它在容器中间的位置。因此,你最终看到的效果和前面所示是一样的:
从《CSS 中 auto
值你知道多少》 一文中,auto
值用于 margin-left
和 margin-right
会计算视窗宽度的一半,减去内容宽度。会让设置了 width
或 max-width
的容器(比如 .container
)中水平居中。同样的,这个可以运用于 padding-left
和 padding-right
上。基于这个原理,我们实现下面这种 Full-Bleed 布局,即容器背景是全屏,内容叠回在全屏的背景上:
实现上图的效果,它的 HTML 结构可以类似下面这样:
<!-- HTML -->
<section class="full__bleed">
<div class="container">
<h1>Page Title</h1>
<p>des...</p>
</div>
</section>
<section>
<div class="wrapper">
<p>....</p>
</div>
</section>
给 section
容器添加相应的CSS:
.full__bleed {
background: url(https://picsum.photos/2568/600?random=1) no-repeat center;
background-size: cover;
}
section {
--limit-container-width: 65ch;
padding-left: calc(50% - var(--limit-container-width) / 2);
padding-right: calc(50% - var(--limit-container-width) / 2);
}
而且当视窗宽度小于或等于 65ch
时,padding-left
和 padding-right
就等于 0
。内容紧挨视窗左右边缘:
如果我们希望在窄屏时,内容距视窗左右两侧也有一定量的边距(比如上面所说的沟槽间距 1rem
)。很多时候都是借助媒体查询来处理:
section {
--limit-container-width: 65ch;
padding: 1rem;
}
@media (min-width: 65ch) {
section {
padding-left: calc(50% - var(--limit-container-width) / 2);
padding-right: calc(50% - var(--limit-container-width) / 2);
}
}
其实,除了媒体查询之外,还可以使用 CSS 比较函数中的 max()
函数。简单地说,设置一个最小的paddding
为沟槽的值--gutter
(约 1rem
),而calc(50% - var(--limi-container-width) / 2)
的计算值作为另一个值:
section {
--limit-container-width: 65ch;
--gutter: 1rem;
padding-left: max(var(--gutter), 50% - var(--limit-container-width) / 2);
padding-right: max(var(--gutter), 50% - var(--limit-container-width) / 2);
}
注意,
max()
函数可以像calc()
函数一样,直接进行四则运算,所以示例中代码并没有calc()
函数!
可以尝试调整浏览器视窗大小,看到的效果如下:
使用 Grid 构建 Full-Bleed 布局
前面我们花了一些篇幅从最古老的布局方式到现代的布局方式,向大家阐述了 Full-Bleed 布局效果是如何构建出来的。随着 CSS Grid 布局越来越成为主流布局时,Full-Bleed 布局变得更为简单和灵活。接下来我们进入今天的核心部分 —— 使用 CSS Grid 构建 Full-Bleed 布局。
Full-Bleed 布局方式和CSS Grid 布局是非常的匹配。为什么这么说呢?主要是因为 Full-Bleed 布局有着自己独特的格式,它是三栏的,“侧栏-沟槽-主栏-沟槽-侧栏”,正匹配着 CSS Grid 中的 grid-template-columns
。
grid-template-columns: [侧栏] [主栏] [侧栏];
gap: [沟槽];
其中,限制性内容使用 grid-area
(或 grid-column
、grid-row
)放置在“主栏”网格区域中,“侧栏”是根据容器可用空间自适应,“沟槽”则是列之间的间距(也就是网格沟槽),全屏的内容则跨越三栏(网格span 3
合并三列,或使用网格线显示放置)。
即:
.container {
display: grid;
grid-template-columns: 1fr 65ch 1fr;
gap: 1rem;
}
CSS Grid 中的 grid-template-columns
结合 gap
属性除了和 Full-Bleed 非常匹配之外,还让 HTML 结构变得简洁:
<!-- HTML -->
<div class="container">
<h1>Page Title</h1>
<p>Des....</p>
<div class="full__bleed">
<img src="" alt="" />
</div>
<p>Des ....</p>
</div>
甚至可以直接使用 <body>
元素来替代 <div class="container">
元素。对应的 CSS 如下:
.container {
--limit-container-width: 65ch;
--gutter: 1rem;
display: grid;
grid-template-columns: 1fr var(--limit-container-width) 1fr;
gap: var(--gutter);
}
grid-template-columns
定义了三个值1fr 65ch 1fr
,这些值定义了每一列的宽度。第一列是 1fr
,第二列是65ch
,第三列和第一列相同,也是 1fr
。列与列之间的间距是 gap
的值,即 1rem
。
fr
单位是 CSS Grid 布局独有的单位,它也是一个灵活的单位,可以填充网格容器可用空间。它的原理有点类似于 CSS Flexbox 布局中的flex-grow
;它是一个比率,表示该列应该消耗多少可用空间。就该例而言,整个容器的可用空间是100vw
,主内容空间是65ch
,间距是2rem
,网格容器可用空间(也称自由空间)就是100vw - 65ch - 2rem
,并且把自由空间分成两等份(有一两个1fr
),也就是说,每个fr
是(100vw - 65ch - 2rem) / 2
,即整个网格容器自由空间的50%
。
我们已经使用 grid-template-columns
显式定义了一个三列的网格,现在我们可以使用 grid-area
或其子属性grid-column
来显示放置网格项目在网格容器中的位置。默认情况下,网格项目会被放置到第一个可用的网格单元。不过我们想覆盖这个默认行为,把所有网格项目放置在中间那一列(主栏),让第一列和第三列空着。
.container > * {
grid-column: 2;
}
在CSS Grid 中,列是以网格线索引号 1
开始,所以中间列对应的是网格线 2
。上面的代码表示我们把所有网格项目放置在第二个中间列。每一网格项目都会创建一个新的行,像下面这样:
正如上图所示,我们已经看到了网格是如何约束所有网格项目的,但是当我们想要让一个网格项目挣脱束缚并填满可用的宽度时(全屏),怎么办?这个时候我们就需要在 .full__bleed
网格项目上使用下面代码:
.full-bleed {
width: 100%;
grid-column: 1 / -1;
}
上面代码中的 grid-column: 1 / -1
相当于 grid-column: 1 / 4
(在该示例),也相当于grid-column-start: 1; grid-column-end: -1
(或 grid-column-end: 4
)。意思是网格项目 .full__bleed
从列网格线1
开始一直跨越到网格线 4
。另外,在网格布局中,数字网格线索引号一般从左往右是一个正数索引值,且从1
开始,同时网格线索引还可以以一个负值索引号,从右往左以-1
开始。
就这个示例而言,还可以使用 span
关键词来指定合并的列数:
.full__bleed {
grid-column: 1 / span 3;
/* 相当于 */
grid-column-start: 1;
grid-column-end: span 3;
}
这个时候,Full-Bleed布局效果已经出来了:
目前效果看上去是 OK 的,其实当你把视窗宽度缩小到小于主栏(第二列)宽度 65ch
时,在水平方向就会出现滚动条:
有接触过 CSS Grid 布局的同学应该知道,CSS Grid 布局是一种非常强大的现代布局技术。在 CSS Grid 布局中改善上面示例中不足之处(视窗屏幕小到一定层度会出现水平滚动条)是非常容易的,比如说,我可以主栏的 65ch
固定值换成一个 minmax()
函数:
minmax(min, max)
函数返回的是一个min ~ max
之间的值,最小的是min
,最大的是max
;如果max
小于min
时,max
将会被忽略,最终minmax(min, max)
将会返回min
值。
假设我们的视窗屏幕的宽度最小是在 320px
(大多设备最小视窗宽度值),放到 minmax()
函数中的话,他就应该是 minmax(320px, 65ch)
,但我们有 2rem
的间距,因此希望在 320px
宽度的屏幕不出现水平滚动条,他应该是 320px - 2rem
,即:minmax(calc(320px - 2rem), 65ch)
。
.container {
--limit-container-width: 65ch; /* 最大宽度 */
--min-viewport-width: 320px; /* 视窗最小宽度 */
--gutter: 1rem;
display: grid;
grid-template-columns:
1fr minmax(
calc(var(--min-viewport-width) - var(--gutter) * 2),
var(--limit-container-width)
)
1fr;
gap: var(--gutter);
}
你可能已经想到了,虽然这样做得到了一定的改善,但当视窗宽度小于 320px
还是会出现水平滚动条。不过不用担心,我们可以在 minmax()
函数中使用内在尺寸,即 min-content
。
.container {
--limit-container-width: 65ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
1fr minmax(min-content, var(--limit-container-width))
1fr;
gap: var(--gutter);
}
这个时候,不管你视窗屏幕有多小,都不会再出现水平滚动条:
除了使用 minmax()
函数之外,还可以在 grid-temlate-columns
中使用 CSS 比较函数,比如 min()
和 clamp()
函数来指定主栏宽度。先来看 min()
函数:
.container {
--limit-container-width: 65ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
1fr min(var(--limit-container-width), 100% - var(--gutter) * 2)
1fr;
gap: var(--gutter);
}
通过使用 min()
函数来选择较小的那个值。在大屏幕上,min()
函数返回的是 65ch
,在较小屏幕上,如果视窗宽度小于 65ch
时,它就会返回 100% - 2rem
,这里使用 100% - 2rem
是因为设置了网格沟槽:
也可以把上面示例中的min()
函数更换成 clamp()
函数,比如 clamp(23ch, 100% - 2rem, 65ch)
。它的意思是主栏的最佳宽度是 100% - 2rem
,在较大宽度的视窗下,100% - 2rem
会大于 65ch
,或在较小宽度的视窗下,100% - 2rem
会小于 23ch
。clamp()
函数会随着视窗大小的改变,返回一个 23ch ~ 65ch
宽度(当然,视窗宽度小于 23ch
会出现水平滚动条):
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
1fr clamp(
var(--limit-min-container-width),
100% - var(--gutter) * 2,
var(--limit-max-container-width)
)
1fr;
gap: var(--gutter);
}
也可以 min()
和 minmax()
函数混合着来用:
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
1fr
minmax(min(var(--limit-min-container-width), 100% - var(--gutter) * 2), var(--limit-max-container-width))
1fr;
gap: var(--gutter);
}
这样做,即使视窗宽度小于 23ch
也不会出现水平滚动条:
上面这些示例,都是在网格容器中显式设置 gap
的值来控制窄屏时,主内容和视窗两侧边缘有一定的安全距离。其实,我们可以在两个侧边栏使用 minmax()
函数,让侧边栏在窄屏时有一个值,这个值就是前面 gap
的值:
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
minmax(var(--gutter), 1fr)
minmax(
min(var(--limit-min-container-width), 100% - var(--gutter) * 2),
var(--limit-max-container-width)
)
minmax(var(--gutter), 1fr);
row-gap: var(--gutter);
}
在使用 CSS Grid 布局时,除了使用网格线索引号来放置网格项目之外,其实还可以使用网格名称来放置网格项目,只不过需要先使用 grid-template-columns
(或 grid-template-areas
声明网格区域)。简单地说,我们在使用 grid-template-columns
声明网格轨道尺寸的同时,可以显式给网格线命名:
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
[full__bleed-start] minmax(var(--gutter), 1fr) [main-start]
minmax(
min(var(--limit-min-container-width), 100% - var(--gutter) * 2),
var(--limit-max-container-width)
)
[main-end]
minmax(var(--gutter), 1fr) [full__bleed-end];
row-gap: var(--gutter);
}
然后在 grid-column
或 grid-area
属性上就可以使用已命名的网格线名称来明确放置网格项目位置:
.container > * {
grid-column: main-start / main-end;
}
.container .full__bleed {
width: 100%;
grid-column: full__bleed-start / full__bleed-end;
}
上面的示例,使用浏览器开发者工具中的网格审查器,就可以看到已命名网格线位置:
我想大家体会到了使用 CSS Grid 实现 Full-Bleed 布局是多么简单,多么灵活了吧。除此之外,CSS Grid 布局还有其他的灵活之处。比如前面示例中,在全屏图片上面层叠文字或其他内容,是把全屏图片当作 .full__bleed
容器的背景图片,然后文字内容居中处理。针对于这样的布局效果,在 CSS Grid 布局中是非常容易的。在运用今天所介绍的 Full-Bleed 布局技巧之外,只需要运用上一节所介绍的 CSS Grid 布局中重叠布局技术,即可实现。
我们来看一个简单的示例:
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
[full__bleed-start] minmax(var(--gutter), 1fr) [main-start]
minmax(
min(var(--limit-min-container-width), 100% - var(--gutter) * 2),
var(--limit-max-container-width)
)
[main-end]
minmax(var(--gutter), 1fr) [full__bleed-end];
row-gap: var(--gutter);
}
.container > * {
grid-column: main-start / main-end;
}
.container .full__bleed {
width: 100%;
grid-column: full__bleed-start / full__bleed-end;
grid-row: 3;
}
.overlay__full--bleed {
grid-row: 3;
z-index: 2;
}
最后再来看一种像下图这样的 Web 布局效果:
这种布局效果也是 Web 种常见的一种布局效果,他有着自己的特色,比如有全屏的、距离左侧或右侧有一定空白空间的:
如果我们把整个布局分成三列,类似于 Full-Bleed 布局,左侧和右侧都是自适应的,主栏是固定宽度的。那么:
- 全屏就是跨越网格三列
- 距离左侧有一定空白空间就是跨越两列,而且是从主栏起始到右侧栏结束位置
- 距离右侧有一定空白空间就也是跨越两列,而且从左侧栏起始到主栏结束位置
用 CSS 来描述的话就像下面这样:
.container {
--limit-max-container-width: 65ch;
--limit-min-container-width: 23ch;
--gutter: 1rem;
display: grid;
grid-template-columns:
[full__bleed-start full__bleed--left-start] minmax(var(--gutter), 1fr)
[main-start full__bleed--right-start]
minmax(
min(var(--limit-min-container-width), 100% - var(--gutter) * 2),
var(--limit-max-container-width)
)
[main-end full__bleed--left-end]
minmax(var(--gutter), 1fr) [full__bleed-end full__bleed--right-end];
row-gap: var(--gutter);
}
.container > * {
grid-column: main-start / main-end;
}
.container .full__bleed {
width: 100%;
grid-column: full__bleed-start / full__bleed-end;
}
.container .full__bleed--left {
width: 100%;
grid-column: full__bleed--left-start / full__bleed--left-end;
}
.container .full__bleed--right {
width: 100%;
grid-column: full__bleed--right-start / full__bleed--right-end;
}
使用开发者工具的网格审查器,可以看到相应的网格参数:
拖动浏览器,改变视窗大小,你看到的效果如下:
事实上,可以把网格列轨道定义的更细,网格项目放置位置不同,得到的效果也将不同。如果你感兴趣,可以发挥你的想象力,使用 CSS Grid 可以构建出更有创意的布局效果。