Subverting the Cascade

The cascade is what makes CSS powerful and special. And yet, sometimes we want to limit that power for containment purposes.

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;
}
NB:

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.