首页 / 文章

CSS attr() 函数:将 HTML 属性值直接用于样式设置

了解如何通过 typed CSS 的 attr() 函数将数据属性转换为长度、颜色和时间值,其备用方案为何会失效,url() 为何会被限制使用,以及如今该如何安全地实现这一功能。

发布者:REACTAPP.TOP阅读约 18 分钟4056 词

来源: CSS attr() Reads Your HTML Now

想象一个进度条,其数值87已经以data-fill="87"的形式存在于标记中。直到最近,要将这个数值设置到width属性上还需要一段简短的脚本:读取dataset.fill的值,加上百分号,然后再将这个自定义属性写回原来的元素。该数值离开文档后又会立即被重新写入,这不仅需要额外的脚本,还会导致一次额外的绘制操作。

attr()函数省去了这一繁琐步骤。该函数现在可以接受不同的数据类型,因此样式表可以将属性值视为长度、颜色、数字或标识符,并将其用于任何能够接受该类型的属性中。本指南将详细介绍该函数的语法、四种查找解析方式、可替代常见脚本的模式、对URL的安全限制,以及如何在浏览器支持尚不完善的情况下推出这一功能。

从字符串生成器到类型化值

经典的 attr() 函数自2015年左右起在所有主流浏览器中均可使用,但它仅承担一个功能:为伪元素上的 content 属性生成字符串。

/* the version you already know */
blockquote::after {
  content: " — " attr(cite);
}

这种功能仅适用于输出引用或链接目标,除此之外别无他用。将旧的 attr() 函数放在 width 属性中会导致整个声明被忽略,因为该函数仅能返回字符串,而 width 属性并不需要字符串类型。

CSS Values and Units 第5级规范改变了这一规则:现在可以告诉浏览器该属性包含何种类型的值。一旦引擎确定处理的是长度、颜色或数字,这些值就可以被用于允许该类型的任何属性中,实际上几乎涵盖了所有属性。

/* the version this article is about */
.bar   { width: attr(data-fill %, 0%); }
.chip  { background-color: attr(data-brand type(<color>), #888); }
.dial  { rotate: attr(data-deg deg, 0deg); }
.card  { view-transition-name: attr(id type(<custom-ident>), none); }

有两点需要澄清,以便让大家的期望保持现实,因为这项功能很容易被过度宣传。

  • 这并非将 JavaScript 从样式中移除的方法。 直接在 HTML 中写入 style="--fill: 87%" 从来都不需要脚本。新变化的范围更小:以前在需要脚本的样式中使用属性值,而现在则不需要了。
  • 这也不会将应用程序逻辑移入 CSS 中。 应用程序仍然会判定任务已完成 87%。唯一的区别在于从元素中读取该数值的主体:是你编写的代码,还是原本就会应用的样式表。

该功能的结构

其语法只需一行即可表达。

attr( <attr-name> <attr-type>? , <fallback-value>? )

使用 attr(data-fill %, 0%)。它包含三个部分:要读取的属性名称、用于解析该属性的类型,以及当查找未得到可用值时的默认值。

该类型可以用三种不同的形式表示。第一种是 type(),它将任何 CSS 数据类型用尖括号包裹起来。

/* 1. type() — any CSS data type */
color:  attr(data-brand type(<color>), crimson);
width:  attr(data-w type(<length>), 0px);
scale:  attr(data-s type(<number>), 1);

第二种形式是单纯的单位标识,表示“该属性存储的是纯数字;将其与该单位关联起来”。第三种是 raw-string,当未指定类型时它也是默认形式,并能复现历史上的行为。

/* 2. unit shorthand — the attribute holds a bare number */
width:           attr(data-w px);      /* data-w="10"    → 10px  */
rotate:          attr(data-deg deg);   /* data-deg="45"  → 45deg */
animation-delay: attr(data-i s);       /* data-i="2"     → 2s    */
width:           attr(data-fill %);    /* data-fill="87" → 87%   *//* 3. raw-string — the default, and the old behaviour */
content: attr(data-name raw-string, "stranger");
content: attr(data-name);   /* identical */

单位简写要求使用无单位的数字

attr(data-w px) 的含义是“读取 data-w 中的数值并将其视为像素值”。因此该属性必须设置为 data-w="10"。若写成 data-w="10px" 则无法解析,因为单位应由样式表指定而非标记语言。当希望 HTML 自行标注单位时(例如编辑器可能输入 2rem 或 10px),应改用 type(<length>) 并存储完整数值。

无效的备用方案会提前以不同方式失败

在浏览器检查该元素之前,回退值会与声明的其他部分一起被解析。如果你编写 attr(data-w px, banana),则在解析时该声明即为无效并被完全忽略:该属性永远不会被读取,取而代之的是规则中之前对该属性的声明。这与属性缺失的情况属于完全不同的错误,后者要直到后续值计算时才会被发现。在脑海中将这两种情况区分开来,能大大简化调试过程。

可声明的类型

type() 函数接受以下类型,同时说明了该属性应包含的内容以及各自的典型使用场景。

  • <length>:用于 width、padding 或 inset 的值,例如 "12px" 或 "2rem"。
  • <percentage>:值为"87%",用于width或translate属性。
  • <length-percentage>:取前两种值中的任意一个,用于需要指定尺寸的属性。
  • <number>:值为"4.5",用于opacity、scale属性,或作为calc()函数的输入值。
  • <integer>:值为"3",用于grid-column、z-index或order属性。
  • <color>:值为"#4fc3f7"或"tomato",用于color和background-color属性。
  • <angle>:值为"45deg"或"0.25turn",用于rotate属性或渐变角度。
  • <time>:值为"300ms",用于animation-delay和transition-duration属性。
  • <resolution>:类似"2dppx"的密度值,由image-resolution属性使用。
  • <custom-ident>:类似"card-3"的标识符,便于为view-transition-name或anchor-name等属性命名。
  • <string>:带引号的CSS字符串,通常由content属性使用。
  • <transform-function>:类似"rotate(20deg)"的表达式,与transform属性一起使用。
  • <image>和<url>:虽存在于语法中,但由于下文安全部分所述原因实际上无法使用。
  • *:可接受任何值,主要用于功能检测。
  • 数字需要通过calc()才能转换为尺寸

    <number>是没有维度的。单独使用4.5无法设定宽度,因此几乎总是要放在calc()中处理。最简洁的方法是先将其存储在自定义属性中,再在该属性中进行运算。

    .stars {
      --r: attr(data-rate type(<number>), 0);
      width: calc(var(--r) * 20%);   /* 4.5 → 90% */
    }
    

    中间出现的 --r 并非仅是形式上的存在。它允许你在多个声明中重复使用该值,而且由于自定义属性具有继承性,这一机制还能让该值传递给后代元素。正如你将看到的,这是与子元素共享属性的唯一方式。如果你已经在脚本和样式之间使用自定义属性进行数据传递,那么关于运行时自定义属性的文章则介绍了这一机制的另一面。

    查找如何解决:四种结果,而非两种

    人们很容易认为该功能要么正常工作,要么规则会被忽略。实际上存在四种不同的结果,混淆这些结果是最常见的意外来源。

    • 该属性不存在。如果指定了备用值,则使用该备用值;否则结果将为确定的无效值,且该属性在计算值时刻即变为无效(IACVT)。
    • 该属性存在但为空(data-x="")。使用带类型的attr()时,空字符串不会被解析为对应类型,因此会使用备用值。而使用无类型的查询则视为成功,返回空字符串,此时会跳过备用值。
    • 该值无法解析为声明的类型。如果有备用值则使用它;否则属性在计算值时刻即变为无效(IACVT)。
    • 该值可以成功解析。即可获取该值,且属性一旦改变样式就会立即更新。

    “在计算值时刻无效”究竟意味着什么

    在解析过程中会捕获到常规的语法错误。浏览器会忽略该声明,而同一规则中更早出现的相同属性声明将会生效。IACVT则发生在级联处理已经选定某个有效声明之后,此时已无其他选项可退而使用。该属性的行为就如同被设置为unset一样:继承来的属性会采用父元素的值,而非继承的属性则恢复为初始值。

    .bar {
      width: 50%;                  /* cascade drops this: the next one parsed fine */
      width: attr(data-fill %);    /* attribute missing → IACVT → width: auto */
    }
    /* result: auto. NOT 50%. */
    

    这就是为什么必须始终提供备用方案的原因——这并非个人偏好问题。如果没有备用方案,拼写错误的属性名就会默默地采用auto、0或继承来的颜色值,而控制台则不会显示任何错误信息。

    为何未指定类型的attr()无法生成颜色

    raw-string 的默认设置值得特别关注。假设某规则规定background-color的值来自一个未指定类型的属性,而你又在仅该属性值不同的元素上测试这一规则。正如预期,没有该属性的元素会使用默认值。而所有具有该属性的元素都会显示为透明,即便其中包含完全有效的颜色名称也是如此。

    原因在于,未加类型的 attr() 会将属性的原始文本作为 CSS 字符串替换进去,而非解析后的 CSS 值。background-color 不接受字符串形式,因此在计算值时替换就会失败。备用方案也无济于事:它仅能处理属性缺失或类型声明不匹配导致的解析错误,而此规则中并未进行任何类型声明。查找步骤本身能够成功返回字符串,但在后续判断是否为颜色时却失败了,因为没有相应的处理机制。

    /* data-c="red" — and the element still renders transparent */
    .swatch { background-color: attr(data-c, gold); }
    

    结论很简单:只要目标属性不是 content,就必须声明其类型。

    替代脚本的模式

    团队们过去常用几行 JavaScript 完成的许多操作,现在完全可以放在样式表中实现。

    从数据属性获取宽度

    标记中直接包含一个普通数字。

    <div class="bar" data-fill="87"></div>
    

    样式表会使用百分比简写来读取该值,若失败则回退为空横条。

    .bar {
      height: 8px;
      border-radius: 99px;
      background: linear-gradient(90deg, #4fc3f7, #2dd4bf);
      width: attr(data-fill %, 0%);
    }
    

    在CMS中选择的颜色

    .chip { background-color: attr(data-brand type(<color>), #888); }
    

    内容编辑器挑选的品牌颜色属于数据而非设计元素。通过使用带参数的attr()函数,可以直接驱动样式表,而无需在模板中编写内联样式。由于<color>的验证是在浏览器中进行的,因此编辑时的拼写错误只会导致显示灰色回退值,而不会造成CSS错误。

    错开出现的列表动画

    通常通过在渲染循环中设置内联样式来实现元素错开出现的效果。不过,索引本身已经是标记的一部分,无需再转换为样式属性。

    <li data-i="0">Inbox</li>
    <li data-i="1">Drafts</li>
    <li data-i="2">Sent</li>
    

    每个元素都会将其自身的索引作为数字读取,然后通过calc()函数将其转换为延迟时间。

    li {
      --i: attr(data-i type(<number>), 0);
      animation: fade-in .45s both;
      animation-delay: calc(var(--i) * .09s);
    }
    

    如果该属性已经包含完整的持续时间,就可以直接使用 s 这一单位简写来表示。

    /* or, if the attribute already holds a duration */
    li { animation-delay: attr(data-delay s, 0s); }
    

    通过 --i 来指定索引还有一个好处:时间间隔会变成一个统一的 CSS 常量。只需在一个地方修改 .09s,网站上的所有错开显示的列表就会随之采用新的节奏。

    每张卡片都有唯一的 view-transition-name

    这种模式实现了之前无法实现的功能。参与视图过渡的每个元素都必须拥有自己唯一的 view-transition-name,而以前则需要为每张卡片生成一条规则或通过脚本来分配名称。现在只需一条规则即可处理所有卡片。

    .card {
      view-transition-name: attr(id type(<custom-ident>), none);
      view-transition-class: card;
    }
    

    请注意,这里的属性是 id,而非 data-* 属性。attr() 可以读取元素上的任何属性,包括 id、href、title、colspan 或 lang。data-* 命名空间只是 HTML 用于存放自定义页面数据的区域。将该名称与 view-transition-class 结合使用,即便每个卡片都有唯一的名称,也能通过同一个选择器为所有卡片的过渡效果设置样式。

    验证这些模式的一种有效方法是将基于脚本的基准配置暂时移除,然后确认样式依然存在;如果样式仍然显示,那就说明是 attr() 在起作用。

    伪元素:无解决方案的情况

    本指南中提到的其他所有方法都有替代方案:你始终可以直接为元素设置自定义属性。但伪元素则不同。element.style针对的是元素本身,而::before或::after并非DOM节点,因此不存在可附加内联样式的对象。之前的选择要么是从宿主元素继承的自定义属性(这仍然需要以某种方式对宿主元素进行样式设置),要么是自动生成的样式表。相比之下,宿主元素上的属性可以直接被伪元素读取。

    这是一个仅以单个属性作为数据的计量器。

    <div class="meter" data-pct="72"></div>
    

    标签在content中使用了通用的字符串函数attr(),该函数在所有浏览器中都能正常工作。

    .meter::before {
      content: attr(data-pct) "% complete";
    }
    

    进度条通过类型化形式,利用同一个属性来设置其宽度。

    .meter::after {
      content: "";
      display: block;
      height: 10px;
      width: attr(data-pct %, 0%);
      background: linear-gradient(90deg, #fbbf24, #f472b6);
    }
    

    一个属性为两个伪元素提供数据,而这两个伪元素均不需要额外的子元素。计数徽章、进度环、工具提示文本、序数前缀、单位后缀以及图表数据标签都遵循这种模式:伪元素需要一个数字,而该数字其实已存在于宿主元素中。在使用 attr() 之前,这个数字必须被重复写入两次——一次作为文本的属性,另一次作为几何形状的自定义属性,而且这两处的数据很容易出现不一致。现在则只需写入一次即可。

    attr()仅读取正在被样式化的元素

    该函数从规则所匹配的元素中读取属性,不会查看其祖先或兄弟元素,也没有类似 closest() 的功能。由于有备用方案存在,这种错误会悄无声息地失败,这也是几乎每个人都会犯过这个错误的原因。

    考虑这样一个段落,它为其中的内容指定了强调色。

    <section class="theme" data-accent="#4fc3f7">
      <h2>Title</h2>
    </section>
    

    从标题中读取该属性并无实际作用,因为标题本身并不包含该属性。

    /* WRONG — this reads the h2, which has no data-accent */
    .theme h2 { color: attr(data-accent type(<color>), #888); }
    /* every heading is #888, forever, with no error */
    

    解决方法是仅在拥有该属性的元素上读取一次该属性,并通过自定义属性将值传递下去。

    /* RIGHT — read it once on the element that has it, then inherit */
    .theme    { --accent: attr(data-accent type(<color>), #888); }
    .theme h2 { color: var(--accent); }
    .theme a  { border-bottom: 2px solid var(--accent); }
    

    继承机制负责实现值的传播。此外,现在任何子树都可以通过普通的 --accent 声明来覆盖强调色,而直接的 attr() 方法则无法实现这一点。

    为何 URL 不可被使用

    attr() 生成的每个值都会被标记为 attr()-tainted,而这种被污染的值绝不能用于通过任何路径构建 URL。以下这两种声明均无效:

    span[data-icon] { background-image: url(attr(data-icon)); }
    span[data-icon] { background: image-set(attr(data-icon)); }
    

    污染会随值一同传递。无论是通过自定义属性进行路由、用src()或image()封装,还是将其嵌套在其他函数中任意深度,都会使污染痕迹留在原处。无论它最终出现在URL的哪个位置,在计算值时该声明都会失效。

    该威胁模型解释了相关规则。属性值往往受用户影响,比如显示名称、CMS短链接以及评论标题等。如果样式表能够将这类值转换为网络请求,那么任何能够修改该字段的人都可以让每位访问者的浏览器去获取其指定的地址,以及浏览器通常会附带到此类请求中的所有内容。正是通过在规范中解决这一问题,浏览器厂商才能就推出该功能达成一致。WebKit在Interop 2026上的公告明确指出了这一点:既然安全相关问题已在规范中得到解决,所有浏览器引擎都表示已准备好推出该功能,并实现相互兼容。

    请牢记该保护措施的局限性。属性中的任何内容都可供页面上的所有脚本读取、在源代码中显示,并包含在HTML数据中传输。URL限制只能阻断一种数据外泄途径,无法让属性变得私密,因此绝不要在属性中存放敏感信息。

    逐步检测支持情况并推出功能

    安全的方法是先使用所有浏览器都能理解的通用基础方案,再为支持该方案的引擎添加类型化版本。应从基础方案开始,此处通过内联自定义属性来实现。

    .bar {
      width: var(--fill, 0%);          /* baseline, everyone */
    }
    

    随后让支持该功能的浏览器切换到使用该属性。

    @supports (width: attr(x %)) {
      .bar {
        width: attr(data-fill %, 0%);  /* modern browsers take over */
      }
    }
    

    如果代码需要分支处理,可在运行时进行相同的测试。

    // the same answer at runtime, if you need to branch in code
    if (CSS.supports("width", "attr(x %)")) {
      // typed attr() is available
    }
    

    针对真实属性进行测试

    对于那些使用占位符属性名的检测代码片段要格外小心,比如参考文档中出现的 @supports (x: attr(x type(*)))。实际上并不存在名为 x 的 CSS 属性,而且当支持查询提到浏览器不认识的属性时,所有浏览器都会返回“否”的结果,因此即便在该功能支持的浏览器中测试也会失败。应使用真实存在的属性。@supports (width: attr(x %)) 这种查询方式是可靠的:width 是真实的属性,而 attr(x %) 只有实现了单位简写功能的浏览器才能解析。建议查看最新的 MDN attr() 参考文档,因为其中的示例确实会经过修正。

    依赖级联规则而非 @supports

    有一种更简短的写法可以跳过 @supports 块,让级联规则来决定。

    .bar {
      width: var(--fill, 0%);        /* everyone */
      width: attr(data-fill %, 0%);  /* supporting browsers override */
    }
    

    无法解析width中attr()语法的浏览器会在解析时忽略该声明,从而采用前面的设置。这种行为源于级联规则处理无效声明的方式。不过,使用@supports形式可以明确表达意图,且仅需要多写几行代码,因此如果希望未来读者能清晰理解该行为,建议采用此形式;在依赖简写形式之前,还需在不支持该特性的浏览器中对其进行验证。

    无论采用哪种形式,都应在attr()内部保留备用方案。在支持该特性的浏览器中,若缺少某个属性,不会回退到上方的var()声明;因为attr()声明已胜出级联规则,此时宽度值将变为auto。

    撰写本文时的浏览器支持情况

    支持情况变化迅速,因此请将这些数据视为暂时的参考值,在实际使用前务必在 Can I use 网站上再次确认。截至 2026 年 8 月,全球约有 70% 的用户使用的浏览器已支持 `attr()` 函数:

    • Chrome 133 及更高版本:已支持,且是首个实现该功能的浏览器(详见 Chrome 开发者公告)。
    • Edge 133 及更高版本:已支持,与 Chromium 的发布同步。
    • Firefox 155 及更高版本:已支持;在 152 至 154 版本中存在但默认处于禁用状态。
    • Samsung Internet 29 及更高版本:已支持。
    • 桌面版 Safari:尚未正式发布,仅存在于技术预览版中。
    • iOS 版 Safari:尚未正式发布,由于所有 iOS 浏览器都使用 WebKit,因此该功能也适用于所有 iOS 浏览器。

    content 中仅支持字符串的旧版 attr() 是另一个问题。它早在十年前就已作为 Baseline 标准广泛存在,贯穿于各大引擎的最初版本之中。如果您只需要 content: attr(data-label),则无需采取上述任何预防措施即可直接使用。

    Interop 2026将带类型的attr()列为重点关注领域之一。Interop是一项年度合作项目,苹果、谷歌、微软和Mozilla以及Igalia和Bocoup会共同挑选至少在一个引擎中缺失或存在缺陷的功能,并承诺在一年内加以改进。进展情况会通过基于共享的Web Platform Tests的公开仪表板进行跟踪。这是表明Safari支持正在开发的最有力的公开信号,但这只是承诺而非发布日期。建议为2026年的剩余时间准备备用方案。如需了解在安全基准之上逐步推出新CSS的更全面方法,请参阅安全发布现代CSS。

    选择attr()还是JavaScript

    这两种工具并非在所有情况下都更优。以下的分类可供实际参考。

    以下情况建议使用attr():

    • 标记本身已包含该值。将其提取到脚本中只是为了在同一节点上再次设置它,毫无意义。
    • 必须放在第一帧中。解析器创建元素时该属性就已存在,而脚本会在之后执行,可能导致可见的修正。
    • 伪元素需要该值。通过element.style设置的行内样式无法作用于::before或::after。
    • 您使用了严格的内容安全策略。行内样式需要unsafe-inline或随机令牌,而数据属性则不需要。
  • 元素是动态添加的。新插入的节点会从现有的规则中获取样式;无需再次运行任何代码,也不需要使用MutationObserver来监控节点结构。
  • 你需要一个唯一的真实数据源。脚本化方法往往会在属性和内联样式中同时存储相同的数据,这两者很容易出现不一致。
  • 你希望样式处理完全由CSS负责,以便能够被级联规则、媒体查询和容器查询所使用,同时也能在浏览器的样式检查器中查看。
  • 以下情况应继续使用JavaScript:

    • 该数据没有理由存在于DOM中。来自ResizeObserver或滚动处理函数的数值不应先被转换为属性。
    • 它会更新每一帧动画。直接调用 el.style.setProperty('--x', v) 更为简洁,无需手动编写属性。
    • 数据量较大、结构嵌套或属于敏感信息。属性中的内容都是公开可见的字符串。
    • 当前必须使用 Safari,且数值非常重要。在某个浏览器中进度条始终显示为零属于缺陷,而非合理的备用方案。

    第一项列表中完全没有提及性能问题。设置内联样式从来都不是性能瓶颈。这样做的好处是能让数值有更好的存储方式且更易于查看,而非提升速度。

    潜在问题清单

    • 务必提供备用方案。如果没有备用方案,缺失或无法解析的数值会导致 IACVT 错误以及 unset 状态,而非保留之前的声明值。
  • 空属性并非缺失属性。若未指定类型,data-x=""会返回空字符串,且备用方案永远不会被触发。
  • 未指定类型意味着为纯文本。当data-c="red"时,background-color: attr(data-c, gold)不会渲染任何内容;需为该属性指定类型。
  • 不存在url()和image-set()功能。这种设计是刻意为之,且会延续到自定义属性中。
  • 仅读取匹配的元素对应的值。对于父级元素,需将相关值存储在自定义属性中以实现继承。
  • 单位简写形式必须使用纯数字。应使用data-w="10",而非data-w="10px"。
  • 无效的备用方案会在解析阶段就导致声明失效,这与属性缺失造成的错误在发生时间和性质上都有所不同。
  • <number>本身无法单独用于设置尺寸。应将其存储起来,然后在calc()函数中再进行计算。
  • 属性是公开可见的。请勿将敏感信息放入其中。
  • 应使用真实属性进行功能测试,例如@supports (width: attr(x %))。
  • 在撰写本文时,Safari尚未实现该功能,因此对于任何关键功能都需要准备备用方案。
  • 总结

    attr()这一写法仅是细微的语法改动,却对架构产生了显著影响:原本就存在于HTML中的值不再需要通过脚本才能转化为样式。这一特性强调了精确性的重要性——需明确指定类型,始终设置备用值,从拥有该属性的元素中读取该值并通过自定义属性传递下去,同时要认识到URL永远无法被使用。在Safari版本发布之前,可将每项核心功能都与基础方案搭配使用,如此该模式即可立即投入实际应用,随着兼容性的提升,样式表会自动接管相关功能。如需确切的解析规则,可参考CSS Values and Units Level 5草案中的正式定义。