图解CSS:Grid布局案例之构建 Full-Bleed 布局

发布于 大漠

Full-Bleed 是印刷界中的一个概念,被称为 全出血,即在印刷中,我们有出血量,这是纸张被修剪的地方以外的区域。正因如此,印刷设计师习惯于在设计工作中考虑出血量。我们通过设置安全区域来做到这一点。

这几年,这种被称为“全出血”的概念也运用到 Web 的布局中。就是在受限宽度的一列中使用全宽元素的布局,比如在较窄的一列文本中使用一个边缘到边缘的图像:

在社区中,也有人把这种布局效果称为 Full-Width 布局,也有人称为 Edge-To-Edge 布局。说实话,在Web中实现这种布局效果,已不是难事,社区中有很多种不同的技术方案,都可以达到这个布局效果。不过,今天我们以不同的角度来思考这个问题!

我们的目标

我们今天最大目标是 在限定宽度的容器中实现全屏布局效果。别的咱先别说,先从设计的角度来看:

在限制容器内放置内容,从而提高页面内容可读性。但有时,可能会在 Web 中出现某个图片需要全屏效果。简言之,就是在某限制宽度的容器内实现全屏的效果。

比如下图的效果:

我们可以看到,我们真正追求的并不是一个有边距的固定宽度的容器。它看上去更像是一个三栏式的布局,一个主栏的两侧有两个沟槽。大多数的内容将只填充中间部分(主栏),但我们的超级花哨的视觉内容(比如图像)将横跨所有三栏。

简单地分析一下 Full-Bleed

前面我们已经说了,Full-Bleed 布局,看上去就是一个三栏布局,只不过他有着自己的特色:

  • 主栏有一个固定宽度,比如 65ch,而且水平居中,即距离视窗两侧边缘距离相等,相当于 margin-leftmargin-right 值为 auto
  • 两个侧栏相等,即可主栏距离视窗两侧边缘相等,相当于 margin-leftmargin-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-widthmargin-left 以及 margin-rightauto让其水平居中:

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

简单地分析一下,容器.containermax-width65ch(宽屏下),窄屏是 width: 100%(相当于视窗全屏)。他们之间相差就是 65ch - 100vw,但我们只需要向左拉出其值的一半,也就是 (65ch - 100vw) / 2 。在这个基础上,还可以进行优化。 65ch 相当于 100%,这样一来 .full__bleed 就可以是:

.full__bleed {
    width: 100vw;
    margin-left: calc(50% - 50vw);
}

这里的关键是,不能把 .full__bleedwidth 设置为 100%,因为百分比计算是相对于其父容器 .container,也就是说 .full__bleed.container 等宽,但它实际要的宽度是 100vw。简单地说,.full__bleed设置width: 100%将会使其失去作用。

不过,父元素的宽度(该示例是 max-width)是有用的,因为 .full__bleedmargin-left50%,告诉浏览器,将元素左边缘对准其父元素的中心,这样做,将左边缘设置为父元素宽度一半。最后,使用 calc()50%的边距中减去50vw。记住,50vw等同于视窗宽度的一半(100vw / 2)。这就是它在容器中间的位置。因此,你最终看到的效果和前面所示是一样的:

从《CSS 中 auto 值你知道多少》 一文中,auto 值用于 margin-leftmargin-right 会计算视窗宽度的一半,减去内容宽度。会让设置了 widthmax-width 的容器(比如 .container)中水平居中。同样的,这个可以运用于 padding-leftpadding-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-leftpadding-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-columngrid-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 会小于 23chclamp() 函数会随着视窗大小的改变,返回一个 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-columngrid-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 可以构建出更有创意的布局效果。