图标如何对齐文本

图标如何对齐文本

当前的最佳实践是,创建一个局部的容器,生成不可见文本(零宽空格,模拟 strut),可使任意内联元素能对齐邻近的文本:

const StrutAlign = ({children}) => (
  <span style={{display: 'inline-flex', alignItems: 'center'}}>
    {'\u200b'/* ZWSP(zero-width space) */}
    {children}
  </span>
)

<p>
  <StrutAlign><MyIcon ></StrutAlign>左侧的任意按钮都能与我对齐
</p>
<p>
  <StrutAlign><MyImage ></StrutAlign>左侧的任意图片都能与我对齐
</p>



关于对齐的常见问题

  1. 浏览器本身如何对齐图标和文本?
  2. 如何封装一个图标组件,使其无论大小如何变化,都能自动对齐临近的文本?
  3. 为什么明明使用了 flex,图标还是看起来差了 1 像素没对齐?

有两种让浏览器自行对齐的方式(CodePen demo:浏览器对齐):

一、 flex container

<button style={{display: 'inline-flex', alignItems: 'center'}}>
  <Icons.Heart />
  Like
</button>

缺陷:需要创建 flex 容器来包裹图标和文本,而不是仅仅通过修改图标的样式。

二、 vertical-align middle

<button>
  <Icons.Heart style={{verticalAlign: 'middle'}} />
  <span style={{verticalAlign: 'middle'}}>Like</span>
</button>

缺陷:需要创建额外标签将文本包裹起来,并且文本偏移了 baseline(将影响上下间距和相邻元素的对齐)。


虽然上面的方式达到了对齐效果,但浏览器本身如何决定图标放置,参照什么来放置?

可以先从字体特征与 CSS vertical-align 开始探索:

图 1:vertical-align

简要说明:

  • 最左侧是一个已对齐的图标,尺寸为 1.2em
  • 最右侧文字标注了横向参考线,除 cap-line 外都是 vertical-align 的可选值
  • vertical-align 作用于文本和非文本(如图片元素)效果不同,以应用 vertical-align: baseline 样式为例:
    • 同一行内不同字体类型、字体大小或不同行高的文本对齐在相同 baseline 上
    • 同一行内不同尺寸的图片底边对齐在 baseline 上
  • xHeight 为小写字母 x 的高度(CSS 有 ex 单位表示它,1ex 约为 0.5em 左右 ), vertical-align: middle 的定义就是它的一半高度,因此仅仅对图片应用 vertical-align: middle样式图片会看起来太靠下
  • capHeight 为大写字母高度(CSS/JS 均无法获取,多数字体约为 0.7em

近似对齐:放置在 cap-line 与 baseline 中间

从图 1 的直接猜测是把图片对齐在大写字母中间,即在 baseline 往上移动 capHeight 的一半。

这种对齐有两种实现方式(CodePen demo:Cap 对齐):

一、从 baseline 开始偏移

默认,图标底边贴在 baseline 上,先移动图标自身 50% 使图标中间对齐 baseline,然后上移capHeight的一半。

/* 计算公式:capHeight / 2 / unitsPerEm ≈ 0.35em */

.center {
  vertical-align: baseline;
  transform: translateY(calc(50% - .35em))
}

/* 或者将 `capHeight` 可能成变量,动态获取 */
/* translateY(calc(50% - var(—capHeight, .35em))) */

缺陷:添加 CSS 动画会因占用了属性而冲突。

二、middle-line 开始偏移

图标中央就在 middle-line 上,因此先下移xHeight的一半,再上移capHeight的一半。

/*
// 计算公式
top
  = (xHeight/2 - capHeight/2) / unitsPerEm 
  ≈ .5em/2 - .7em/2
  ≈ -.1em

// 或复用 ex 单位来计算
top
  = 1ex/2 - capHeight/2 / unitsPerEm
  ≈ calc(0.5ex - 0.35em)
*/

// JSX
<button>
  <Icons.Heart
    style={{
     verticalAlign: 'middle', 
     position: 'relative', 
     top: '-.1em',
   }} 
  />
  Like
</button>

无论选择哪种偏移方式,偏移值都会根据当前字体不同而有细微差异,见下图的高亮列:

图 2:Font Metrics

精确对齐:对齐定高文本

「精确」指达到与浏览器自行对齐相同的效果。需求来自问题 2,如何封装一个图标组件,仅通过修改图标本身来对齐相邻文本。

图 3:三种 line-height

这个图很有趣,内联元素在不同条件下产生了不同的边界(颜色填充区域),简要说明(CodePen demo:浏览器本身如何对齐):

  • j 表示基础的 line-height,也就是 1em 高,等同于当前的 font-size 100px,当 inline-block 元素(也可以是 inline-flex item)设置为 line-height: 1 时可以得到它。值得注意 j 的边界不在任何参考线上。此图也能说明不要误用这个值,有可能(如应用 overflow 样式)内容溢出导致截断。
  • x 表示安全的 line-height,是 inline 元素默认的高度(也是 text-toptext-bottom 的距离)。也可以由图 2 中的 (ascender + descender) / unitsPerEm 计算得到(当前字体对应表格中第三行的值,1.1777)。
  • S 表示实际的 line-height ,是 inline-block 或 inline-flex 元素默认高度(也是topbottom 的距离)。当设置父容器 line-height: 1.7 时,实际 line-height 将是 100px * 1.7 = 170px

有了上面的图例,精确对齐思路可以借鉴 CSS 规范中的 strut 概念,创建一个局部的容器,生成不可见文本(零宽空格,模拟 strut),让不可见文本对齐 line-box 中其他文本,让图标对齐这个不可见文本:

On a block container element whose content is composed of inline-level elements, 'line-height' specifies the minimal height of line boxes within the element. The minimum height consists of a minimum height above the baseline and a minimum depth below it, exactly as if each line box starts with a zero-width inline box with the element's font and line height properties. We call that imaginary box a "strut."

局部容器的高度可以是上图 S 的高度(inline-flex 居中),也可以跟随图标的高度(图标绝对定位,保持与容器位置相同),也可以固定(图标绝对定位居中,但缺陷是不能太大超过行高)。

精确对齐的两种实现方式(CodePen demo:模拟浏览器对齐):

一、inline-block hack

const MyIcon = ({width, height}) => (
  <span
    style={{
      position: 'relative',
      display: 'inline-block',
      lineHeight: height, // 使文本高度为图标高度
      width, // 占住横向空间
    }}
  >
    {'\u200b'/* ZWSP(zero-width space) */}
    <svg
      width={width}
      style={{position: 'absolute', left: 0, top: 0}}
    />
  </span>
)

对齐效果完美。

二、inline-flex hack(推荐)

const MyIcon = (props) => (
  <span style={{
    display: 'inline-flex',
    alignItems: 'center',
  }}>
    {'\u200b'/* ZWSP(zero-width space) */}
    <svg {...props} />
  </span>
)

对齐效果完美(不支持过时的浏览器 caniuse.com/flex)。


为什么明明使用了 flex,图标还是看起来差了 1 像素没对齐?

图标与文本的对齐误差取决于 iconSizefontSizelineHeight 之间奇偶关系(不同浏览器表现可能不同,CodePen demo: 奇偶对齐):

\begin{array} \text{iconSize} & \text{fontSize} & \text{lineHeight} \\ \hline 奇数 & 偶数 & 奇偶改变不影响图标对齐\\ \hline 偶数 & 奇数 & 奇偶改变不影响图标对齐\\ \hline 偶数 & 偶数 & 偶数对齐,奇数偏上 1px\\ \hline 奇数 & 奇数 & 奇数对齐,偶数偏上 1px\\ \hline 小数(.2/.5/.8)& 奇数 & 奇偶改变不影响图标对齐,都上偏 1px(偶数时图标纵向拉伸了 1px)\\ \hline 小数(.2/.5/.8) & 偶数 & 偶数对齐,奇数偏上 1px(偶数时图标纵向拉伸了 1px)\\ \end{array}\\

?测试环境:字体 PingFang SC,浏览器 Chrome,对齐 Flex。

相关

图标转 React 组件工具,集成了本文提到的对齐属性(center):

编辑于 2019-10-18 16:25