Subverting the Cascade
Let’s say we’re designing the obligatory card component. Simpleton that I am, I’ve decided all we need is a border and background color:
:root {
--card-accent: cadetblue;
}
.card {
border-color: var(--card-accent);
background-color: color-mix(in srgb, var(--card-accent), #FFF 75%);
}
Our component definition might impose restrictions on the card’s title (e.g. limiting it to plain text1), but allow authors to use arbitrary markup for any remaining content.2
Inevitably, we end up with nested cards:
Note that we’ve highlighted an individual card by customizing --card-accent
via its style
attribute; that’s intentionally a part of our component’s
contract.
But what if we highlight our top-level card instead?
That’s a bit garish! Nested cards inherit the customization; that’s not what we want here.
Enter @property
Browsers now enable us to put constraints on custom properties via
@property
definitions,
meaning we can suppress inheritance for this particular customization option:
@property --card-accent {
syntax: "<color>";
inherits: false;
initial-value: cadetblue;
}
inherits
is not yet
supported
in Firefox.
Having said that, in this particular case we could have just moved the
custom-property definition into our .card
rule set instead:
.card {
--card-accent: cadetblue;
border-color: var(--card-accent);
background-color: color-mix(in srgb, var(--card-accent), #FFF 75%);
}
So we don’t actually need this newfangled bit of CSS here. In fact, after
realizing this, I now can’t think of many use cases for inherits
anymore –
though that might be due to aphantasia. 🤷
I was originally hoping to employ this for my
ubiquitous .stack
utility in order to
allow for localized custom spacing:
.stack > * {
margin-block: 0;
& + * {
margin-block-start: var(--stack-spacing, --spacing);
}
}
However, I couldn’t make that work because .stack
is all about descendants and
thus relies on inheritance to some extent. Plus apparently rem
doesn’t work
for <length>
properties? 🧐 🤷
@scope
to the Rescue?
Maybe @property
wasn’t the right tool in the first place: With the advent of
@scope
we’ll be
able to limit selectors’ reach without foregoing inheritance wholesale.
I’m not entirely sure yet how that might apply to the challenges described above, so I’ve created a separate demo instead:
Here both button
and .title
styling is limited to my-card
descendants
not within section
, thus excluding the checklist:
@scope (my-card) to (:scope section) {
.title {
font-variant: small-caps;
}
button {
border-radius: 100%;
}
}
A significant advantage there is that we can use simple, readable element
designations instead of attempting to disambiguate via globally unique class
names (à la BEM, not to mention
crimes against the web). This works
because @scope
allows us to distinguish controlled and open constituents of
our component.