Generally speaking, the modern attr() syntax is backward-compatible because the old way of using it — without specifying an <attr-type> — behaves the same as before. Having attr(data-attr) in your code is the same as writing attr(data-attr type(<string>)) or the simpler attr(data-attr string)).
However, there are two edge cases where the modern attr() syntax behaves differently from the old syntax.
In the following snippet, browsers that don't support the modern attr() syntax will discard the second declaration because they cannot parse it. The result in those browsers is "Hello World".
div::before {
content: attr(text) " World";
}
div::before {
content: attr(text) 1px;
}
In browsers with support for the modern syntax, the output will be … nothing. Those browsers will successfully parse the second declaration, but because it is invalid content for the content property, the declaration becomes "invalid at computed value time" or IACVT for short.
To prevent this kind of situation, feature detection is recommended.
A second edge case is the following:
<div id="parent"><div id="child" data-attr="foo"></div></div>
#parent {
--x: attr(data-attr);
}
#child::before {
content: var(--x);
}
Browsers without support for modern syntax display the text "foo". In browsers with modern attr() support there is no output.
This is because attr() — similar to custom properties that use the var() function — get substituted at computed value time. With the modern behavior, --x first tries to read the data-attr attribute from the #parent element, which results in an empty string because there is no such attribute on #parent. That empty string then gets inherited by the #child element, resulting in a content: ; declaration being set.
To prevent this sort of situation, don't pass inherited attr() values onto children unless you explicitly want to.