首页
/ Bulma `.field` 表单容器详解:`.control` 职责分离与 field 布局体系的演进

Bulma `.field` 表单容器详解:`.control` 职责分离与 field 布局体系的演进

2026-09-05 18:34:47作者:侯霆垣

Bulma 在 0.6.0 版本中引入 .field 元素,将表单控件从"既能做块级容器、又能做行内容器"的 .control 中解耦出来,从而支持横向 help 文本、带图标的 addons、多控件分组加载等更复杂的表单布局。本文基于仓库中的发布文档 New field element,结合当前源码 sass/form/tools.scss,完整讲解这次 API 重构的背景、前后写法对照,以及 .field / .field-label / .field-body / .control 的现行实现与可定制变量。

背景:.control 的双重身份与它的代价

从 Bulma 早期版本开始,.control 一直是表单控件的"万能容器",一个元素同时承担了六类职责:

  • 让控件在垂直方向上拉开间距
  • 在 input、button、textarea 旁边附加图标
  • 为控件追加 loading 旋转器
  • 创建横向表单(horizontal form);
  • 创建 control addons(连体控件,如搜索框 + 按钮);
  • 创建 control groups(控件分组)。

问题在于它同时扮演了两种容器角色:

角色 用途 典型修饰类
块级(block)容器 垂直间距、横向表单中的行容器 块级边距行为
行内(inline)容器 图标、loader、addon、grouping .has-icon.is-loading

这种混合导致三个做不到的场景(这也是 .field 出现的直接动因):

  1. 无法在横向表单中放 help 文本 —— help 文本需要与 .control 同级而不是嵌套其中;
  2. 无法在一组控件里同时挂多个图标或 loader —— 图标/loader 的修饰类只能挂在单个 .control 上;
  3. 无法给 addons 中的每个控件挂不同图标 —— addon 组整体共享一个 .control 上下文。

核心变化:.field 承接 block 角色,.control 回归纯行内

发布文档给出的结论只有一句话:

TL;DR: there's a new .field container, and .control has been re-purposed.

具体分工调整为:

  • .field 成为 .control 的 block 容器,并继承了原先挂在 .control 上的布局修饰类:.has-addons.is-grouped.is-horizontal
  • .control 只允许包含一个 .button.input.select.textarea,外加(至多)一个 .icon;它不能再包含 .help 元素,也不能再嵌套其他 .control

类结构前后对照(摘自发布文档):

// Before
.control
  .has-addons
  .has-icon
  .is-grouped
  .is-horizontal
  .is-loading

// After
.control
  .has-icon
  .is-loading
.field
  .has-addons
  .is-grouped
  .is-horizontal
.field-label
.field-body

HTML 写法的前后对照(以"用户名可用"提示为例):

<!-- Before -->
<label class="label">Username</label>
<p class="control has-icon has-icon-right">
  <input class="input is-success" type="text" placeholder="Text input" value="bulma">
  <span class="icon is-small">
    <i class="fas fa-check"></i>
  </span>
  <span class="help is-success">This username is available</span>
</p>

<!-- After -->
<div class="field">
  <label class="label">Username</label>
  <p class="control has-icon has-icon-right">
    <input class="input is-success" type="text" placeholder="Text input" value="bulma">
    <span class="icon is-small">
      <i class="fas fa-check"></i>
    </span>
  </p>
  <p class="help is-success">This username is available</p>
</div>

结构上的关键差别:.label.help.control 内部提升为 .field 的直接子元素,与 .control 平级。这正是"help 可以横向排布"得以成立的原因——它们与控件同处一个 flex/block 容器层级。

注意:当前仓库源码中图标修饰类已改名为 has-icons-left / has-icons-right(见 sass/form/tools.scss.control.has-icons-left, &.has-icons-right 的选择器),发布文档里的 has-icon / has-icon-right 是 0.6.0 时期的历史写法,在后续版本中被弃用。

.field 的现行实现:三个布局修饰类

发布文档中 .field 只列出了"继承了哪些修饰类",而当前源码 sass/form/tools.scss 则展示了这些修饰类的完整实现。

.field 基础样式:继承 block 行为

.field {
  @include cv.register-vars(
    (
      "block-spacing": #{cv.getVar("field-block-spacing")},
    )
  );

  @extend %block;
  ...
}

从源码结构看,.field 通过 @extend %block 复用全局 block 的垂直间距行为(%block 定义在 sass/utilities/extends.scss),并把自己专属的块间距变量 --bulma-block-spacing 指向 CSS 变量 field-block-spacing。这个变量由 SASS 变量 $field-block-spacing 注册,默认值 0.75rem,同样定义在 sass/form/tools.scss

$field-block-spacing: 0.75rem !default;

.field 之间的垂直节奏由它统一控制,这正是"垂直拉开控件间距"这一原 .control 职责的落点。

.has-addons:连体控件

&.has-addons {
  display: flex;
  justify-content: flex-start;

  .control {
    &:not(:last-child) {
      margin-inline-end: -1px; // 边框重叠,视觉上连成一体
    }
    &:not(:first-child):not(:last-child) {
      .button, .input, .select select {
        border-radius: 0; // 中间段无圆角
      }
    }
    &:first-child:not(:only-child) { /* 只保留左侧圆角 */ }
    &:last-child:not(:only-child)  { /* 只保留右侧圆角 */ }

    &.is-expanded {
      flex-grow: 1;
      flex-shrink: 1;
    }
  }

  &.has-addons-centered { justify-content: center; }
  &.has-addons-right    { justify-content: flex-end; }
  &.has-addons-fullwidth {
    .control { flex-grow: 1; flex-shrink: 0; }
  }
}

要点:addons 通过负外边距让相邻控件边框重叠、对非首尾控件清零圆角来实现"连体"外观;每个子控件用 .control.is-expanded 声明弹性扩展;交互状态(hover/focus/active)通过 z-index: 2/3/4 分层,保证聚焦控件的 focus 光圈不被相邻控件压住。

.is-grouped:带间距的控件组

&.is-grouped {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-start;

  & > .control {
    flex-shrink: 0;

    &.is-expanded {
      flex-grow: 1;
      flex-shrink: 1;
    }
  }

  &.is-grouped-centered   { justify-content: center; }
  &.is-grouped-right      { justify-content: flex-end; }
  &.is-grouped-multiline { flex-wrap: wrap; }
}

与 addons 的区别在于用 gap: 0.75rem 保留控件间距(不连体),并提供居中/靠右/多行三个变体。每个 .control 默认 flex-shrink: 0 不被压缩,仅 .is-expanded 的控件参与伸缩。

.is-horizontal.field-label / .field-body

&.is-horizontal {
  @include mx.tablet {
    display: flex; // 仅 tablet 及以上生效
  }
}

.is-horizontal 在移动端保持块级堆叠、在 tablet 断点以上才切换为 flex 横排,是响应式表单的基础。与之配套的两个子容器:

  • .field-label:tablet 以上 flex-grow: 1text-align: right、右侧 1.5rem 间距,并支持 is-small / is-normal / is-medium / is-large 四档字号(每档带 padding-top: 0.375em 与输入框顶部对齐);移动端仅保留 0.5rem 的下边距。
  • .field-body:tablet 以上 flex-grow: 5(label:body 形成 1:5 的列比例),内部多个 .field 之间 margin-inline-end: 0.75rem,非 .is-narrow 的子 field 自动 flex-grow: 1 平分剩余宽度。

这一 label:body = 1:5 的弹性比例,是从源码中可以直接验证的布局细节。

.control 的现行实现:纯行内职责

重构后的 .controlsass/form/tools.scss 中只剩与"单个控件内部装饰"相关的逻辑:

.control {
  box-sizing: border-box;
  clear: both;
  font-size: cv.getVar("size-normal");
  position: relative; // 为图标/loader 的绝对定位提供参照
  text-align: inherit;
}
  • 图标定位.has-icons-left / .has-icons-right 下,.icon 以绝对定位覆盖在输入框上(height/width 均取 input-heightz-index: 4),输入框则按 input-height 预留左右 padding;input 的 hover/focus 通过兄弟选择器 & ~ .icon 联动图标颜色。
  • loading 旋转器.is-loading::after 伪元素 @extend %loader 渲染,定位于控件右上角内(inset-inline-end: 0.75em; top: 0.75em),并随 is-small / is-medium / is-large 调整字号——因为 .control 现在只包一个控件,spinner 不再与其他控件互相干扰,这正是发布文档"解决方案"一节中"允许多个控件各自挂 loader"的落地形态。
  • 控件度量基线.control 本身的高度、行高、内边距来自 sass/utilities/controls.scss$control-height: 2.5em$control-line-height: 1.5$control-padding-vertical / -horizontal 等变量,统一注册为 --bulma-control-* CSS 变量,供 input、select、button、file 等所有控件形态共用。

%control placeholder 在 sass/utilities/extends.scss 中定义,被各输入形态 @extend,保证同一控件在不同上下文中度量一致。

文档中的两个典型示例

发布文档给出了两个最能体现新结构的场景。

示例一:addons 组内每个控件各自带图标/状态

<div class="field is-grouped">
  <p class="control is-expanded has-icon">
    <input class="input is-success" type="text" placeholder="Username" value="alexsmith">
    <span class="icon is-small">
      <i class="fas fa-check"></i>
    </span>
  </p>
  <p class="control is-expanded has-icon">
    <input class="input is-warning" type="email" placeholder="Email" value="alex@smith.com">
    <span class="icon is-small">
      <i class="fas fa-exclamation-triangle"></i>
    </span>
  </p>
  <p class="control is-expanded is-loading">
    <input class="input" type="email" placeholder="Name">
  </p>
</div>

三个控件分处三个独立 .control,各自携带不同的图标或 loading 状态——这在旧结构(一个 .control 包整组)中是做不到的。

示例二:横向表单 + help 文本

<div class="field is-horizontal">
  <div class="field-label">
    <label class="label">From</label>
  </div>
  <div class="field-body">
    <div class="field is-grouped">
      <p class="control is-expanded">
        <input class="input" type="text" placeholder="Name">
      </p>
    </div>
    <div class="field">
      <p class="control is-expanded has-icon has-icon-right">
        <input class="input is-success" type="email" placeholder="Email" value="alex@smith.com">
        <span class="icon is-small">
          <i class="fas fa-check"></i>
        </span>
      </p>
      <p class="help is-success">This email is correct</p>
    </div>
  </div>
</div>

嵌套关系值得注意:外层 .field.is-horizontal.field-label + .field-body 分成左右两栏,.field-body 内部再嵌套两个 .field(一个 is-grouped 的 Name 组、一个 Email 字段),help 文本位于内层 .field 中、与 .control 平级——这是 .help 能正常换行排布的前提。该示例与现行文档 Input 表单页面.input.is-static 横向表单的示例结构完全一致,说明这一写法在后续版本中持续沿用。

.label.help:field 的配套元素

.field 重构的完整体验还包括两个文字元素,定义同见 sass/form/tools.scss

  • .labeldisplay: block,颜色取 text-strong$label-color),字重 $label-weight 默认 weight-semibold,非末位时 margin-bottom$label-spacing(默认 0.5em),支持 is-small / is-medium / is-large 字号变体。
  • .helpdisplay: block,字号 $help-size 默认 size-smallmargin-top: 0.25rem;通过 @each 遍历 $label-colors(即完整调色板)自动生成 is-{color} 变体,与 .input.is-success.input.is-warning 等状态色一一对应。

这些变量($label-color$label-spacing$label-weight$help-size$field-block-spacing)都带 !default,可以在自定义 SASS 中覆盖;$field-block-spacing 对应的 CSS 变量为 --bulma-field-block-spacing(见 CHANGELOG.md 中 v1 的变量迁移记录),即项目已同时支持 SASS 变量与 CSS 变量两条定制路径。

版本演进脉络

结合 CHANGELOG.md 可以还原这条 API 线的完整演进:

版本 变化
0.5.x .control.is-grouped 使用 .control 元素作为直接子元素(BREAKING,见 0.6.0 记录)
0.6.0(2017-03) 新增 .field 元素;.control 重新定义职责 —— 即本文发布文档对应版本
0.7.2 .control.has-icon 弃用,改名为 .control.has-icons
1.x 表单样式全面接入 CSS 变量($field-block-spacing--bulma-field-block-spacing 等),.field 继续 @extend %block

从 CHANGELOG 到当前 sass/form/tools.scss 的选择器可以验证:0.6.0 确立的".field 管 block 布局、.control 管单控件装饰"这一分层在 1.x 中完全没有被推翻,只经历了命名(has-iconhas-icons-left/right)与变量机制(SASS → CSS 变量)的渐进式迁移。

小结

.field 的引入是 Bulma 表单系统一次典型的"单一职责"重构:

  1. .field:block 容器,负责垂直间距与三大布局修饰类(.has-addons / .is-grouped / .is-horizontal),并新增 .field-label.field-body 支撑横向表单的响应式 1:5 布局;
  2. .control:行内容器,只包裹一个 .button / .input / .select / .textarea 与可选 .icon,负责图标定位(has-icons-left/right)与 loading 旋转器(is-loading);
  3. .label / .help:作为 .field 的直接子元素与 .control 平级,获得横向排布能力,并与全调色板状态色联动。

对使用方的实际意义:从旧版迁移时,把原先"一个 .control 包一切"的结构拆开——labelcontrolhelp 提升为 .field 的三个兄弟节点;图标与 loading 修饰类下沉到具体控件的 .control 上;布局修饰类上提到 .field 上,即可获得发布文档所述"更复杂的表单设计"能力,且与现行 1.x 源码结构完全兼容。

登录后查看全文
热门项目推荐
相关项目推荐