前端知库

CSS @property 如何让自定义属性参与类型校验和动画?

解释普通 CSS 自定义属性为什么按 token 保存、不能可靠插值,说明 @property 的 syntax、inherits 和 initial-value,以及注册属性在主题、动画、组件封装和兼容性中的实际边界。

参考答案

简答

  • 未注册的自定义属性按一串 token 保存,浏览器不知道它是百分比还是颜色,所以 transition: --x 会直接跳到终点。
  • @property 声明 syntax、inherits、initial-value,让浏览器按类型计算和插值,属性才能平滑动画。
  • 不符合 syntax 的值在计算时无效,回退到继承值或初始值;但语法校验不等于业务范围校验。
  • 注册是全局名字约定,组件私有属性要加前缀;不支持时要退回普通自定义属性,动画只当增强。

三个描述决定属性的语义

CSS
@property --progress {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

.bar {
  width: var(--progress);
  transition: --progress 300ms ease;
}

.bar[data-progress='done'] {
  --progress: 100%;
}
  • syntax:允许的值类型,如 <length>、<color>、<number>,各自有不同的插值规则;
  • inherits:false 时子元素不继承,拿到的是 initial-value;
  • initial-value:除 syntax: '*' 外必须提供,且必须是与上下文无关的计算值(例如 0px 可以,1em 不行)。

注册后把 --progress 设成 50px 不符合语法,会回退到初始或继承结果;未注册的属性则会原样保存这个字符串。

inherits 怎样选

组件内部的进度、角度、缩放等局部状态适合 inherits: false,避免父级同名变量污染;主题色、字体、密度等 token 需要往下传,应保持 true。不同库注册同一个名字会共享同一条注册规则,所以私有属性要用稳定前缀。

语法匹配不等于业务校验

<length> 能排除明显不是长度的值,但不会检查“不能为负”“不超过 2rem”这类业务约束,这些仍要在组件 API 或运行时校验。变量展开后不满足目标属性语法时,整条声明在计算时失效,复杂的多层 fallback 要在开发者工具里确认结果。

颜色动画与可访问性

CSS
@property --card-color {
  syntax: '<color>';
  inherits: false;
  initial-value: transparent;
}

.card {
  background: var(--card-color);
  transition: --card-color 200ms ease;
}

.card[data-active='true'] {
  --card-color: rgb(219 234 254);
}

颜色插值空间要按目标浏览器验证。动画只改视觉,用颜色表示错误时仍要配合文字、图标或 aria-invalid。

JavaScript 注册

JavaScript
if ('registerProperty' in CSS) {
  CSS.registerProperty({
    name: '--progress',
    syntax: '<percentage>',
    inherits: false,
    initialValue: '0%',
  });
}

同名属性重复调用 CSS.registerProperty() 会抛出 InvalidModificationError,组件库要保证只执行一次,并考虑热更新和多副本加载。静态的公共属性更适合写在 CSS 里,便于阅读和审查。

兼容与性能

支持范围以 MDN 兼容表为准;不支持时属性仍按普通自定义属性生效,只是没有类型化动画和非继承行为。注册本身很少是瓶颈,关键是它控制什么:每帧改变 <length> 会触发布局,比只改 transform 或 opacity 成本高,应在 Performance 面板实测。