Skip to content

[css-mixins-1] Need a non-hacky way to force an element-dependent value on the applying element #13454

@tabatkins

Description

@tabatkins

Due to hygienic renaming and the mixin evaluation rules, code like the following:

	@mixin --triple-border(--size <length>) {
		@result {
			&, & > *, & > * > * {
				width: var(--size);
				border-width: .2em;
			}
		}
	}
	section {
		font-size: 10px;
		--size: 10px;
		@apply --triple-border(10em);
	}
	section > h1 {
		font-size: 20px;
		--size: 20px;
	}
	section > h1 > small {
		font-size: 15px;
		--size: 15px;
	}

Will desugar to approximately:

	section {
		--f7bd60b7: 10em; /* and it's registered as a <length> so it turns into 100px here */
		font-size: 10px;
		--size: 10px;
		width: var(--f7bd60b7); /* 100px;*/
		border-width: .2em; /* 2px */
	}
	section > h1 {
		font-size: 20px;
		--size: 20px;
		width: var(--f7bd60b7); /* 100px */
		border-width: .2em; /* 4px; */
	}
	section > h1 > small {
		font-size: 15px;
		--size: 15px;
		width: var(--f7bd60b7); /* 100px */
		border-width: .2em; /* 3px */
	}

That is, the var() referencing a Mixin argument is captured on the applying element and maintains a single stable value across all uses, but the em length isn't touched, and resolves based on the element it's used on.

This isn't an undesirable behavior in general - it'll probably often be the case that you do indeed want the styles you put on children to resolve on the children. But I think it'll also be common to want to capture a value on the applying element and use that on the children, but right now the only way to do that is with an argument that you just document the user shouldn't pass, so you can set a default value, like:

	@mixin --triple-border(--size <length>, --em <length>: 1em) {
		@result {
			&, & > *, & > * > * {
				width: var(--size);
				border-width: .2--em;
			}
		}
	}
	section {
		font-size: 10px;
		@apply --triple-border(10em);
		/* Everybody gets a 100px width and a 2px border-width, using this element's font-size */
	}

That's awkward and clumsy, tho. You might think you could use a local variable with a type-coercing function, like this:

	@function --as-length(--x <length>) returns <length> { result: var(--x); }
	@mixin --triple-border(--size <length>) {
		--em <length>: --as-length(1em);
		@result {
			&, & > *, & > * > * {
				width: var(--size);
				border-width: .2--em;
			}
		}
	}

But that doesn't work. The evaluation rules lift that local variable into being a local variable in an anonymous function called on each element, and custom functions inherit font-size from the calling element, so the 1em will still be evaluated based on each element's local font-size instead.


There's a few possible solutions to this. I think the most obvious is to make local variables act exactly the same as Mixin parameters, and lift+hygiene them onto the applying element. We'd have to lift the entire Mixin body, tho, since the local variable might be set inside an @media. That means even more "unobservable" things on the applying rule, but perhaps that's fine once you've already bitten the bullet of having an unobservable custom property in the first place. (This would also make the "inside-out" and "outside-in" desugarings actually identical; see Example 20 at the end of https://drafts.csswg.org/css-mixins/#evaluating-mixins.)

If that's no good and we have to maintain the different behavior for parameters and locals, then we can at least add something "in-between" the two, which is lifted like a parameter but solely under author control like a local. That's right, we'd be reviving the using(...) clause, letting it add "shadow" parameters that are evaluated as if they were unpassed arguments, just resolving to their default value (which would be required in the grammar for the using-list).

I'd definitely prefer the first solution, tho, as locals and parameters are otherwise pretty identical. Thoughts? @andruud @emilio (I dunno who, if anyone, from WebKit is appropriate to ping for opinions on these issues)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Wednesday afternoon

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions