嗨,聊点烧脑的。昨天看了几篇文章,脑子里突然像过电一样,把几个看似八竿子打不着的东西串起来了。有人聊组件和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不是相对于页面根元素,而是相对于:host的font-size,因为Shadow DOM会创建一个新的“根”上下文。这就完美模拟了那个“基础单元”的想法。而且,每个组件实例都能独立设置自己的基准字号,互不打架。WCAG要求文本可以缩放,这里通过改变一个属性,整个组件内部的文字比例就能统一缩放,同时保持布局不乱,无障碍方面自然就优化了。如果设计系统更新了某个按钮的样式,只需要改组件模板里的样式代码,所有用到这个组件的地方,重新构建一下,就全部更新了。
这几种路子走下来,组件自己有了“主见”,能根据环境或者外部指令调整自己的“身段”。改一个地方,不用满世界找依赖它的兄弟组件。无障碍也不再是单个组件的“个人秀”,而是整个页面里所有组件默契配合的结果。说到底,让组件之间能好好说话、互相适应,比单个组件打磨得再光鲜都管用。
