CSS单位、组件与无障碍,这三者到底怎么才能凑到一块儿去?

3,014字
13–19 分钟
in

嗨,聊点烧脑的。昨天看了几篇文章,脑子里突然像过电一样,把几个看似八竿子打不着的东西串起来了。有人聊组件和WCAG无障碍标准的那些事儿,有人琢磨设计系统自动化更新带来的连锁反应,还有人突发奇想,说CSS是不是该搞个介于根单位和相对单位之间的“基础单元”。这三件事单拎出来都够喝一壶的,但把它们揉一块儿想,感觉像是摸到了点什么东西。别急,这就把这点“洗澡时灵光一闪”的念头掰开揉碎了说说,顺便看看怎么把这些概念落地,让代码既听话又省心。

目录

啥是“基础单元”?

这个想法挺有意思,就像在根元素(html)和普通元素之间加了个“传话的”。通常咱们用rem是听根元素的,用em是听父元素的。那如果有个东西,能指定某个元素作为“基准”,让它的子孙元素都从这个基准继承字号啥的,是不是就能解决很多布局上的“上下对不齐”的问题?想象一下,一个卡片组件,它内部所有东西的字号都跟着卡片的标题字号走,而不是听命于页面最顶上的根元素,这样无论卡片被塞到页面哪个犄角旮旯,它的内部比例都稳如老狗。

理想很丰满,现在能咋整?

虽然这个“基础单元”八字还没一撇,但眼下手里的家伙什儿也能捣鼓出类似的效果,让组件之间互相谦让、和谐共处,顺便把无障碍和自动化更新的问题也给捎带上。

方案一:自定义属性玩接力

这个法子最直接,用CSS自定义属性(也叫CSS变量)搭个桥。在组件的根节点上定义字号变量,内部所有相对单位都引用这个变量。

<div class="card" style="--component-font-size: 18px;">
  <h2>标题文字</h2>
  <p>内文描述,字号基于组件基准计算。</p>
  <button class="btn">点我一下</button>
</div>
.card {
  /* 组件内部的基准字号,可以从外部传入或默认设置 */
  font-size: var(--component-font-size, 16px);
}

.card p {
  /* 内文相对组件基准缩小一点 */
  font-size: calc(var(--component-font-size, 16px) * 0.875);
}

.card .btn {
  /* 按钮字号保持与基准一致 */
  font-size: var(--component-font-size, 16px);
  padding: 0.5em 1em;
}

这里面有几个门道--component-font-size这个变量定义在卡片上,卡片内部所有元素都能抓到它。如果想在深色模式下整体调大字号,只需要改这一处变量值,内部所有东西自动跟着动,不用费劲巴拉地找每一个按钮和内文。另外,padding用了em,它相对于当前元素的font-size,而当前元素的font-size又跟着变量走,所以整个组件的内边距也会等比例缩放。这就好比给组件装了个总阀门,拧一下,里面所有的水流都跟着变化。

方案二:容器查询来兜底

容器查询(Container Queries)是另一把利器,它能让组件根据自身宽度调整样式,而不是根据视口宽度。把这个和自定义属性结合起来,简直是天作之合。

<div class="sidebar">
  <div class="card-container">
    <div class="card">
      <h2>侧边栏卡片</h2>
      <p>这里空间窄,字号自动调整。</p>
    </div>
  </div>
</div>
<div class="main-content">
  <div class="card-container">
    <div class="card">
      <h2>主内容区卡片</h2>
      <p>空间宽敞,字号可以更大点。</p>
    </div>
  </div>
</div>
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    --component-font-size: 20px;
  }
}

@container (max-width: 399px) {
  .card {
    --component-font-size: 14px;
  }
}

.card {
  font-size: var(--component-font-size, 16px);
}

实际操作时得留神container-type: inline-size告诉浏览器,这个容器可以响应宽度变化。然后,当容器宽度不同时,给里面的卡片设置不同的自定义属性值。卡片本身不需要关心自己身处何方,它只认那个变量。这样一来,同样一个卡片组件,放到窄的侧边栏里自动变小字号,放到宽敞的主内容区就变大字号,完全不用写两套样式或者靠媒体查询猜它在哪。这不就是设计系统里说的“一处更新,处处生效”吗?改卡片的样式,只需要动.card那块的定义,而字号怎么变,由它的容器说了算,互不干扰。

方案三:作用域样式与Web Components

如果想把组件封得更彻底,Web Components是个好选择。用@scope规则或者Shadow DOM的样式隔离,能保证组件样式只在自己那一亩三分地生效,同时还能从外部接收主题变量。

<custom-card theme-color="blue" base-font="18px">
  <h2 slot="title">可配置的组件</h2>
  <p>内容完全由外部控制,样式内部封装。</p>
</custom-card>
class CustomCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    const baseFont = this.getAttribute('base-font') || '16px';
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          font-size: ${baseFont};
        }
        h2 {
          font-size: 1.5rem;
        }
        p {
          font-size: 0.875rem;
        }
        button {
          font-size: 1rem;
          padding: 0.5em 1em;
        }
      </style>
      <slot name="title"></slot>
      <slot></slot>
    `;
  }
}
customElements.define('custom-card', CustomCard);

这里头的讲究可多了:host代表组件本身,它的font-size由外部传入的base-font属性决定。内部所有元素用rem,但这里的rem不是相对于页面根元素,而是相对于:hostfont-size,因为Shadow DOM会创建一个新的“根”上下文。这就完美模拟了那个“基础单元”的想法。而且,每个组件实例都能独立设置自己的基准字号,互不打架。WCAG要求文本可以缩放,这里通过改变一个属性,整个组件内部的文字比例就能统一缩放,同时保持布局不乱,无障碍方面自然就优化了。如果设计系统更新了某个按钮的样式,只需要改组件模板里的样式代码,所有用到这个组件的地方,重新构建一下,就全部更新了。

这几种路子走下来,组件自己有了“主见”,能根据环境或者外部指令调整自己的“身段”。改一个地方,不用满世界找依赖它的兄弟组件。无障碍也不再是单个组件的“个人秀”,而是整个页面里所有组件默契配合的结果。说到底,让组件之间能好好说话、互相适应,比单个组件打磨得再光鲜都管用。