CSS 与布局 / textarea 样式化 · 一份配方的七条声明 / 完整配方与变量接口 待审核 8 / 8
七条声明合并

完整配方与变量接口

前七页逐条拆过的声明合回一处,就是下面这份样式表。本页先看它作为一个整体的变量接口设计,再给出实测出的一处缺口。

1 · 配方全文

textarea {
  --_rows: calc(var(--textarea-rows, 5) * 1lh);
  --_max-rows: calc(var(--textarea-max-rows, 20) * 1lh);
  --_foreground: var(--textarea-foreground, var(--color-lightest));
  --_background: var(--textarea-background, var(--color-darkest));
  --_border-color: var(--textarea-border-color, var(--color-dark));

  display: block flow;

  /* ① 把 box-sizing 还原成未指定的状态(content-box) */
  box-sizing: unset;

  /* ② 在父容器里铺满 */
  inline-size: -webkit-fill-available;
  inline-size: stretch;

  /* ③ 最小高度取 --_rows 行 */
  min-block-size: var(--_rows);

  padding: 1em;
  border: 1px solid var(--_border-color);
  color: var(--_foreground);
  background-color: var(--_background);

  /* ④ iOS 的缩放对策:字号以 16px 为下限,更大时跟随 1rem */
  font-size: max(16px, 1rem);

  @supports (field-sizing: content) {
    /* ⑤ 最大高度限制在 --_max-rows 行 */
    max-block-size: var(--_max-rows);

    /* ⑥ 关掉用户手动改变尺寸 */
    resize: unset;

    /* ⑦ 启用高度随内容变动 */
    field-sizing: content;
  }
}

原注释的 ③ 写的是「最大高度」而实际是 min-block-size,且称此后 rows 属性失效——两处都与实测不符,订正见 行数尺寸与 lh 单位 §2。上面已按实测改写。

图 1-1 · 配方全文作用在真实控件上。可调两个行数变量与配色,观察 --_rows 的像素落点与实测盒尺寸;末尾代码块输出当前变量下的计算值。

2 · 两段式变量接口

五个私有变量都是同一个形状:

--_rows: calc(var(--textarea-rows, 5) * 1lh);

这个形状同时做了三件事。

其一,划出内部与外部的界线。--_ 前缀是社群约定的「私有」记号——CSS 没有可见性机制,调用方写 --_rows: 120px 照样生效,前缀能表达的只是一句约定:这一组不对外。对外的名字统一带 textarea- 前缀,避免与页面上其他组件的变量撞名。

其二,把单位留在组件内部。调用方传的是数字 5,不是长度 5lh。单位由组件决定,调用方不必知道内部用的是 lh 还是 em,组件日后换算法也不会破坏调用。

其三,给出默认值。var() 的第二个参数是 fallback,变量未定义时用它。配方的 fallback 又是一层 var(--color-lightest),指向项目的公共色板。

警示 · 最后这层嵌套有个前提:--color-lightest 之类必须真的存在。若项目里没有这个变量,整条链在计算值阶段失效,后果按属性是否继承而分叉——实测 background-color: var(--_background) 回到 initial value transparent,控件变透明;而 color 是继承属性,会继承父级的文字色。前者常常直到换了深色背景才被发现。稳妥的写法是给最内层也留一个字面量:var(--textarea-background, var(--color-darkest, #16181d))

3 · 配方漏掉了字体族

font-size: max(16px, 1rem) 接管了字号,但配方没有任何一条声明碰 font-family。在一个没有 reset 的页面上把全文跑起来,实测结果是:字体族仍是 UA 的 monospace,line-height 仍是 normal

这个遗留不止是观感问题,它会顺着 1lh 传到尺寸上。420px 容器、根字号 16px 下的实测读数:

实测值 本该是
font-family monospace 页面正文字体
1lh 19px(monospace 的 normal 行距) 24px(line-height: 1.5
min-block-size 95px 120px
边框盒 420 × 129px 420 × 154px

也就是说「至少五行」这句话是按 monospace 的行距兑现的,与页面正文的五行差出 25px。补法是加一行 font-family: inherit,或者更彻底的 font: inherit

这处缺口还有一个不容易预料的后果:上表里的 129px 是不可移植的。给本系列写数字锁的 e2e 时,同一份配方在 playwright 自带的 Chromium 里量到的是 124px——那个构建里 generic monospace 的 normal 行距是 18px 而非 19px。1lh 的取值绑定在字体的度量上,而 generic family 解析到哪一款字体由平台决定。凡是没写死 line-height 的地方,跨浏览器、跨操作系统都不会得到同一个像素高度;本系列的测试因此对这类值只钉「五行加 34px」这条关系,不钉绝对数。

建议 ·font: inherit 时它必须写在 font-size: max(16px, 1rem) 之前font 是简写,会把 font-size 一并重置为继承值,写在后面就把 16px 下限覆盖掉了。实测在一个 font-size: 13px 的父级里:font-size 在前、font: inherit 在后,控件字号 13px,缩放下限失守;顺序反过来则是 16px。

4 · 这份配方适用到哪里为止

七条声明解决的是一个控件的形态,且假定它独占一行、宽度铺满容器、高度随内容增长。表单里并排的两个短输入框、行内的可编辑标签、需要固定高度以保证页面不跳动的评论框,都不在这个假定里。

尤其要留意 inline-size: stretch 的作用域:它让 margin box 填满 containing block,前提是这个控件的宽度确实该由容器决定。放进 flex 或 grid 容器后,铺满本就由容器的对齐规则负责,这条显式尺寸会与 align-items / justify-items 的效果叠加,两处规则谁说了算需要逐例确认,本系列没有覆盖这种情形。

配方给出的是一份作用于元素选择器的默认值,不是一个组件。它的收益在于把「新加一个 <textarea> 就要重写一遍尺寸」变成不必做的事;代价是它对全站每一个 <textarea> 生效,包括那些本不该铺满的。改哪一条、什么时候整条删掉,仍然是每个页面自己的判断。