Bulma Tiles:用纯 CSS Flexbox 构建 Metro-UI 风格网格
本文基于 Bulma 官方博客《Metro UI CSS grid with Bulma tiles》原始文档 展开:当年 Bulma 借 Flexbox 与单一的 tile 元素实现了经典的 Metro-UI 式磁贴网格布局。文章将完整还原 tile is-ancestor / is-parent / is-child 三层类名架构,结合仓库中保留的演示页与端到端测试,给出每一层元素的精确样式契约(负外边距、flex: 1 1 0、12 步尺寸系统等),并说明该特性在 Bulma v1 中的最终归宿,帮助你在旧代码中读懂 Tiles,在新代码中正确迁移。
一个 HTML 元素实现 Metro UI 网格
Bulma 官方博客原文的核心主张非常简洁:
你是否想过用纯 CSS 构建一个 Metro-UI 风格的网格?得益于 Flexbox 与
tile元素,你现在可以做到了——而且只需要 1 个 HTML 元素:tile元素。
这里的技术前提有两点:
- Metro UI 网格的视觉特征:多个内容块(磁贴)无缝拼接,块与块之间只有均匀的小间隙,可以横向平铺、纵向堆叠、任意嵌套,并且同一行里的磁贴默认等宽伸缩;
- 实现手段:不引入任何 JS,完全靠
display: flex容器 +flex-grow均分 + 负外边距/内边距技巧制造间隙。
从 CHANGELOG 的记录看,.tile 是在 v0.0.19 版本中首次引入的(日志以 “NEW!!! - .tile” 标记),时间上正对应这篇 2016 年 4 月的博客,也就是说 Tiles 是 Bulma 最早期的招牌特性之一。
Tiles 的三层类名架构
Bulma 的 Tiles 并非一个类名打天下,而是把“容器—包裹层—内容块”拆成了三个修饰符,全部挂在同一个 .tile 类上:
| 类名 | 角色 | 职责 |
|---|---|---|
tile is-ancestor |
祖容器 | 最外层 flex 容器,用负外边距把内边距技巧“外翻”,消除网格四周的溢出间隙 |
tile is-parent |
父包裹层 | 提供 12px 内边距,形成磁贴之间的均匀缝隙 |
tile is-child |
内容块 | 真正可见的磁贴(常叠加 box、notification is-info 等既有元素),自身外边距归零 |
仓库中保留的完整演示页 tiles 演示 展示了标准用法,其中嵌套了一个纵向堆叠的子网格:
<div id="tile-ancestor" class="tile is-ancestor">
<div class="tile is-vertical is-8">
<div class="tile">
<div class="tile is-parent is-vertical">
<article class="tile is-child notification is-warning">
<p class="title">Vertical...</p>
<p class="subtitle">Top tile</p>
</article>
<article class="tile is-child notification is-warning">
<p class="title">...tiles</p>
<p class="subtitle">Bottom tile</p>
</article>
</div>
<div class="tile is-parent">
<article class="tile is-child notification is-info">
<p class="title">Middle tile</p>
<p class="subtitle">With an image</p>
<figure class="image is-4by3">
<img src="...">
</figure>
</article>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child notification is-danger">
<p class="title">Wide tile</p>
<p class="subtitle">Aligned with the right tile</p>
</article>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child notification is-success">
<p class="title">Tall tile</p>
<p class="subtitle">With even more content</p>
</article>
</div>
</div>
这段结构值得逐层读:
is-ancestor是最外层行容器,直接子元素是两个“大块”:一个is-vertical is-8(占 12 列中的 8 列、内部纵向排布),一个is-parent(占剩余 4 列);is-vertical把 flex 方向从row翻转为column,于是is-vertical内部的两个is-parent自上而下堆叠,形成典型的 Metro 式“左列两块 + 右侧一整块 + 左下再一块”的拼贴;is-child直接复用其他元素类——例子里叠了notification is-warning / is-info / is-danger / is-success与box,这说明 Tiles 只是布局层,视觉皮肤完全由既有元素提供,这正是“一个元素解决布局”的设计意图;- 磁贴内部还能再嵌
content、image is-4by3等标准组件,层级上不封闭。
从端到端测试还原 Tiles 的精确样式契约
当前仓库中 Tiles 的 CSS 源码虽已随版本演进而移除(在 sass 目录中搜索不到 tile 规则),但仓库保留了完整的 Cypress 端到端测试 tiles.spec.js 与演示页 tiles.html,其中对计算样式(computed style)的断言,恰好是一份精确的样式契约文档。以下数据均可在该测试中逐条验证:
基础 .tile 是均分伸缩的 flex 容器
// docs/cypress/e2e/grid/tiles.spec.js(断言摘要)
expect(cs.alignItems).to.equal("stretch");
expect(cs.display).to.equal("flex");
expect(cs.flexBasis).to.equal("0px");
expect(cs.flexGrow).to.equal("1");
expect(cs.flexShrink).to.equal("1");
expect(cs.minHeight).to.equal("min-content");
display: flex+flex: 1 1 0:所有同级磁贴默认等宽伸缩、平分剩余空间,这正是 Metro 网格“同行等宽”观感的来源;align-items: stretch:子块沿交叉轴自动拉伸到等高,保证一行内的磁贴高度对齐;min-height: min-content:防止内容被压缩到不可读。
间隙 = 负外边距 + 内边距的经典组合
is-ancestor 与 is-parent 的断言揭示了 Bulma 制造缝隙的技巧:
// is-ancestor
expect(cs.marginTop).to.equal("-12px");
expect(cs.marginRight).to.equal("-12px");
expect(cs.marginBottom).to.equal("12px");
expect(cs.marginLeft).to.equal("-12px");
// is-parent
expect(cs.padding).to.equal("12px");
// is-child
expect(cs.marginTop).to.equal("0px");
// ...四个方向的外边距全部为 0
机制可以推断为:每个 is-parent 向内垫 12px 内边距,相邻磁贴自然产生 2 × 12px = 24px 的间隙;而 is-ancestor 用四周 -12px 负外边距把这部分“多出来的”空间抵消掉,避免网格整体外溢。其中 margin-bottom 特例是 12px(而非 -12px),让网格底部与页面后续内容保持一个间隙;当祖先容器位于页面最底部时,测试中的 #tile-ancestor-last 断言其 margin-bottom 变为 -12px,把底部间隙也抹平——这是一个容易忽略的边界细节。
上述 12px 对应 Sass 变量 $tile-spacing。CHANGELOG 在“Grid”变量清单中明确列出了 $tile-spacing,即磁贴间隙是可定制的单一变量,改动一处即全局生效。
12 步尺寸系统:is-1 到 is-12
演示页末尾有一行 12 个磁贴的“标尺”:
<div class="tile is-ancestor" style="width: 1000px;">
{% for i in (1..12) %}
<div id="tile-{{ i }}" class="tile is-{{ i }}">Tile {{ i }}</div>
{% endfor %}
</div>
测试对其逐一定量校验:
expect(cs.flexGrow).to.equal("0");
expect(cs.flexShrink).to.equal("0");
expect(`${Math.round(actualWidth)}px`).to.equal(
`${Math.round((i / 12) * 1000)}px`
);
即:在 1000px 宽的祖先容器内,is-N 磁贴的实测宽度严格等于 N/12 × 1000px,且 flex-grow / flex-shrink 均为 0——尺寸类把磁贴从“均分模式”锁定为“1/12 步进固定模式”。这与 Columns 的 12 列体系同构,学习成本几乎为零:想要 8/12 宽就写 is-8。
is-vertical 纵向排布
测试同时验证了纵向磁贴的两个要点:
// is-vertical 容器
expect(cs.flexDirection).to.equal("column");
// 纵向排列的 child 磁贴之间
expect(cs.marginBottom).to.equal("24px");
flex-direction: column 让同级磁贴自上而下堆叠;而纵向子块之间以 24px(即两个 $tile-spacing)作为间距,与横向 is-parent 内边距合成的 24px 缝隙保持同一节奏,横纵两个方向上间隙观感一致。
特性演变:从招牌特性到 v1 弃用
理解 Tiles 的现状,需要看它的完整生命周期(均出自 CHANGELOG 与迁移指南):
- v0.0.19 引入:日志以 “NEW!!! -
.tile” 标记,是 Bulma 早期网格方案的核心(对应本文开篇的博客); - 后续版本补充变量:Grid 变量清单中加入
$tile-spacing,间隙全面变量化;同一时期的日志中还出现 “test tiles” 的移除记录,说明该模块在维护期经历了反复修剪; - v1 弃用:迁移指南 migrating-to-v1.html 明确写道 “Tiles are deprecated”,并给出 Before/After 对照——Before 正是
tile is-ancestor / is-parent / is-child结构,After 则改用 v1 全新的 Grid 系统(源码位于 sass/grid 目录,含 grid.scss 与 columns.scss)。
因此在今天阅读 Bulma v1 源码时,sass/ 目录下已经找不到 tile 规则——仓库中 Tiles 的完整资料,就集中在这篇博客、演示页 tiles.html 与测试 tiles.spec.js 里。如果你在维护仍依赖 Tiles 的旧版页面,建议按迁移指南把磁贴网格改写为新的 Grid;如果只是想学习“纯 CSS 做 Metro 拼贴”的思路,上文基于测试断言还原的样式契约(flex 均分 + 负外边距间隙 + 12 步尺寸)仍然是一份完整、可复现的实现范本。
小结
- Tiles 用
tile一个类加is-ancestor / is-parent / is-child三个修饰符,构建出容器、缝隙、内容块三层职责清晰的 Metro-UI 式网格,全程零 JS; - 样式契约有测试背书:基础磁贴
flex: 1 1 0且align-items: stretch;is-parent内边距与is-ancestor负外边距各为12px合成24px均匀间隙;is-1~is-12提供 1/12 步进的固定宽度;is-vertical切换为column方向并保持同样的间距节奏; - 该特性诞生于 v0.0.19,在 Bulma v1 中被标记弃用并由新 Grid 系统取代;历史资料可追溯至博客原文、CHANGELOG 与保留的 Cypress 测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00