- Author: Tab Atkins-Bittner
- Created: 2025-06-30
In general, we put "semantics" into HTML, and "presentation" into CSS.
This line is somewhat blurry (display:none
, for example),
but it's usually reasonable to tell them apart.
But sometimes, even for things that "clearly" fall into the semantics half,
we get proposals to put them in CSS,
because they're probably going to be used on a lot of elements,
in a formulaic pattern,
and CSS Selectors are a great feature for that.
This has the theoretical problem that now more semantic content is written into what is "supposed" to be presentational. It has a practical problem that Selectors can actually get really complex and depend on funky things, and that can be a bit of an issue when keeping the a11y tree in sync with the stated semantics. It also splinters the set of features across two different syntaxes, based purely on whether someone believes, early in the design process, that it would be useful to work with Selectors.
It would be nice if we could continue always putting the a11y/etc data into HTML, but allow authors to use a more convenient syntax to apply them when they wish. Then this isn't a design question for individual features, it's just something anyone can do for any attribute.
- Define "Cascading Attribute Sheets" (CAS) (first proposed in https://www.xanthir.com/blog/b4K_0, this is a subset of that proposal). This lets authors choose to write highly repetitious attributes in a much more compact fashion.
- Define the JS equivalent of CAS, to take some pressure off of CAS to do everything. Properties, event listeners, element references, etc can all be done in JS instead, with similar usability.
- Define an expansion of ID references, used in attributes, to allow fuller selectors. (Proposed most recently in https://discourse.wicg.io/t/relative-element-references-in-html/5627/.) This allows more things that might need element refs (and thus require the JS api) to be done in CAS instead, and just generally helps solve the "link two things, but for a bunch of repeated component structures).
A new language, reusing CSS syntax, and invoked via <script type=cas>
.
element.selector > goes[here] {
attr-name: attr-value;
}
- The selectors are the full set of CSS selectors.
- The "property name" is the name of an attribute you're going to set, and the "property value" is the value you're going to set it to. Note that in both cases, casing is preserved;
foo: bar
andFOO: BAR
are two different things, setting<div foo=bar>
or<div FOO=BAR>
. - If the value is a CSS string, we use the string's value; if it's an ident, number, or dimension, we stringify those.
- If there are multiple values, we put in a single space between the values.
- We have a few special values,
#on
,#off
/#unset
, and#initial
, that respectively set it to the empty string (for boolean attributes), unset it entirely (#off
for boolean attributes,#unset
for anything else, tho this is just a convention; they're identical), or leave it unchanged from how it was. - You can also mark a declaration as "optional", where it only sets the attribute if it's not currently set (but it leaves manually-set attributes in the HTML alone), with an
!optional
at the end (like CSS's!important
).
Here's a simple but realistic example:
video {
preload: metadata;
}
#content video {
preload: auto;
}
Execution is done by collecting all the CAS sheets, and when DOMContentLoaded, applying all the rules, using CSS's normal specificity rules. The winning declarations then modify all the attributes. Notably, this is all done in one pass, so an attribute set by one CAS rule can't affect the selector of another CAS rule.
Note
Maybe we want a mechanism to allow for layering, so you can do some setup of more generic attributes, then apply some sheets based on those. Possibly reuse cascade layers? Layers by default shouldn't affect these, so we can continue to use them for specificity reasons as usual, but we can add a rule that indicates which layers are "breakpoints", so earlier layers are matched and executed in their entirety before the breaking layer and later ones are matched/executed. Building on cascade layers feels very natural, as by definition their rules can't be interleaved with earlier layers anyway.
Additionally, CAS sheets set up automatic mutation listeners, listening for inserted elements. Whenever an element is inserted into the document, all CAS rules are matched and executed over it, modifying its attributes.
(This is slightly inconsistent with the initial run - an insertion can implicitly depend on attributes set by other CAS rules on already-inserted elements, but that's unavoidable.)
CAS can only set actual attributes to actual string values. But there's plenty of use-cases for setting DOM properties without associated attributes, and sometimes setting them to element references, or functions, etc. We can't do this in CAS, because setting a property can execute a setter; we need a JS execution context.
To allow these cases to be handled with the same ease as CAS, and the same basic timing/behavior, we introduce a DOM API that does essentially the same thing.
Rough sketch:
CAS.registerRule("selector here", {"attr": "val", "*prop": "val", "onfoo": function(e){...}});
print(CAS.getRules()); // only prints the JS-created ones, I guess?
Like CAS sheets, any JS-created CAS rules are executed at DOMContentLoaded, and on element insertion.
You should also be able to manually re-trigger CAS execution on an element or the entire document, including both CAS sheets and JS-created ones:
CAS.execute(doc); // runs on the entire document
CAS.execute(el); // runs on the element and its children
CAS.executeOne(el); // runs on just the one element, not its children
TODO: fill in more concrete proposal based on https://discourse.wicg.io/t/relative-element-references-in-html/5627/
Regarding
!optional
, I think that perhaps this is better if it follows the CSS patterns, where the CAS sheet provides a value in the case that no attribute is specified, rather than overriding the value in the document. That would allow you to keep!important
, but - more importantly - fits better with the authoring pattern we have with CSS, rather than introducing a different mode.