-
Notifications
You must be signed in to change notification settings - Fork 675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Custom CSS Functions & Mixins #9350
Comments
OK, that seems approachable. Having a
Those two things are not compatible. But it's possible that we'll able to impose enough restrictions on this to make it feasible, and still get something useful. For example, limit mixins to only declarations, or only declarations and nested &-rules (which would allow your nested
That's an interesting idea. :-) |
I think it will be hard to support @mixin --foo {
.a { }
}
.b {
@apply --foo;
}
.c {
@apply --foo;
} In that case we would need to start matching from the .a in the mixin and branch out to the nested .b and .c selectors. This branch is dynamic based on how It was not clear to me from the "Mixins, on the other hand, will mostly contain CSS declarations and nested rules that can be output directly" that nested selectors would be allowed, but the clearfix example in the full explainer uses nesting. |
If nesting is possible it would be popular. Many existing mixins are used to establish things like consistent hover/focus handling (nested pseudo-classes) or like icon-buttons, including styles for the nested icon, etc. (updated so that the variable-substitution reference is specific to functions) |
I always saw Where resolving would be roughly equivalent to wrapping in Is there a specific use case where the result would be substantially different? (I am definitely viewing this to much from the perspective of preprocessors.) Edit :
ack |
That's a whole different story and much simpler to implement. It would not allow you to apply |
That would be unfortunate. Dev tools will often bundle or chunk stylesheets in ways that aren't immediately clear or controllable by authors. It would make mixins much harder to use. |
Really happy to see this proposal! As an author, I always wanted to have mixins and functions in CSS, and can't wait for the day when we would get anything closer to it. I don't mind the exact syntax, so would mostly focus on features and usage.
Is this a strong requirement? I can see a lot of use cases for the implicit access for external custom properties:
Some other questions/thoughts, but very preliminary, as I would need to re-read the proposal (and I just skimmed the explainer for now, would need to read it properly) a few times and think long about it:
That's it for now. I would try to find time to read the whole explainer properly, and would come back with more feedback. |
@lilles Some follow-up about nesting in mixins… I don't think that constraint would be a blocker for making this a useful feature. I'm curious if there's any useful distinction between between nested selectors that change the subject, and those that only add additional constraints? For example, selectors like
@kizu I would consider it a strong authoring requirement that nothing inside the function should have accidental consequences on values outside the function. I don't know that we need such a strong requirement in the other direction. I don't know if there would be implementation concerns about allowing external values into a function, but I think you're right that it could work for authoring.
I'm not familiar with the feature in Stylus, but I think this is similar to the Similarly, there's a section on argument conditions and loops – roughly your points 3 & 2, though I don't go into a lot of detail on lists/maps. In both cases, I considered them potential future extensions rather than essential aspects of a basic function/mixin feature. |
First, I've talked this over with Miriam already, and broadly am happy with the elaboration on my earlier idea of making functions basically just "fancy variables". Edited before posting: whoops, yeah, the edited version of argument syntax makes me a lot happier. I think we can still put syntax into the arglist, fwiw. @function --foo (--bar "<string>", --baz "<number>") {...} I think that still reads reasonably? And if it is too long and hard to read, you can always move it to (I presume that anything without a default value would be a required arg, making the function invalid if not passed? Since args in the arglist wouldn't have a default, that would match up with the usual practice of required args coming first, followed by optional args.)
I disagree with this, and think it's probably pretty important to allow functions to access the variables present on the element. I don't see any way for there to be an accidental conflict; every name that could potentially conflict is function-local and controlled by the function author, no? Tho, hm, we'd have to be a little careful about nested functions. If we did make ambient variables available we'd have to decide whether all functions saw the element's vars, or saw the function's vars too. The latter does have an accidental shadowing concern. But I feel like it's important to allow this, because otherwise you can't replace existing variables (that reference other vars) with functions unless you explicitly pass those variables, which can be annoying. In particular, it means you couldn't use custom units unless they were explicitly passed to the function, which feels extra-obnoxious.
Yes, this sounds great. Temp vars are useful.
I agree that these are useful, but their usefulness isn't specific to functions. We've talked about a (I'm not particularly against doing the Unmentioned here is what to do about recursive functions. Without the ability to test and branch on the value of one of the input variables, I think a recursive call is guaranteed to be cyclic just like a custom-property self-reference is, right? So presumably that should be detected and auto-failed using the existing anti-cyclic variable tech. If we later allow for the possibility of useful recursion we can relax the requirement and impose a stack limit or something. (Then we can repeat the TC39 arguments about tail recursion, yay!) |
I would not presume that, since guaranteed invalid is a reasonable default value. Is there a strong reason that should need to be specified explicitly in an
I'm open to this. As mentioned above, my stronger concern is that changes to an external variable don't accidentally escape the function.
That comment was not about a special syntax for functions, but a normal syntax allowed inside functions. The only potential need for a special syntax would be if we want to allow some (restricted) parameters in function at-rules. But this proposal does not include that as an initial requirement. (It's also not specific to the |
Functional languages tend to have a I've wanted CSS functions for a long time but some of the places I've wanted them I'd only use it once so I could reuse internal computations without leaking a lot of one-time custom properties (my solution in practice is to just shrug and leak a lot of one-time custom properties and hope it doesn't cause any problems later). If there were a CSS version of let-expressions you could just tell people using functions to use that if they need private stuff and it would be handy for the odd case where you have a handful of things you want to refer to more than once but a function would be overkill. |
Right, I assumed that they'd be applied before any value stages, essentially equivalent to doing a preprocessor. As far as I can tell there's nothing dynamic about mixins (save the values passed in themselves, but they're effectively just uniquely-named variables). (And note that we already established, back in the last attempt at That, I think, would allow us a lot of freedom in what to allow inside the mixin without adding a lot of complexity. However, it limits our ability (somewhat) to do branching/etc based on the values. We could still do, say, a |
Sure, but we could spell that And it's not a reasonable default value if you have a syntax. Without the "required" behavior, we'd have to require that with a syntax you also have to provide an initial value.
Agreed, that's definitely required, but I don't think anything could reasonably cause that. You can't affect any properties on the element from within a function.
Yeah, I don't have an issue with the proposal, was just thinking aloud. I think doing conditionals as proposed is better (plus we don't have a plan for an inline CQ function anyway). |
I don't think that would be different. The common thing is that nesting in this case dynamically combines parts of the selectors from the mixin and the apply and that would require the implementations to connect those pieces at a later stage than the parser, and that the multiple applications branch into multiple ancestor selectors. For nesting it's much easier since it's done at parse time and the selector representation can be fully built at parse time. How complicated and how much of a re-organization of the selector matching/invalidation code this is depends when we can resolve the mixin applications. |
IIUC, it is a requirement that mixins can be applied across stylesheets, so that the connection between the mixin and the apply has to be re-done as new stylesheets are added and requires some cascade order definition for which mixin is found first for a given name. As mentioned in a different post I think this can be challenging for an implementation if we allow selectors inside the mixin. At least if the mixin contains a selector which becomes part of multiple nested selectors. |
I'm sorry, Rune, but I don't understand what connection your reply has to what I said. Could you elaborate? I suspect one or both of us is misunderstanding what the other is saying. |
Sorry, I tried commenting on the "essentially equivalent to doing a preprocessor" part. I read "preprocessing" as something that can be done at parse time, which would limit apply to reference mixins in the same sheet. |
Ah, ok, no, I meant it more in the "not dynamic based on the DOM" way; it only requires information that is available to a preprocessor (the full set of stylesheets for a page). I imagine it would be roughly equivalent to just inserting the rules with JS (after uniquifying the input variable names so they can be replaced with normal custom props set in the selector). So, an expensive operation to perform, and it might need to be repeated as you discover more stylesheets, but still generally a one-and-done operation, after which you just have normal style rules. |
I've found a lot of people, including myself sometimes, would prefer to have variables act like references that are lexically scoped, as opposed to names than can be dynamically overridden based on HTML structure and CSS selectors applied to it. I suspect this will even be more true of functions and mixins where the intent of the author is to import and use a specific function or mixin, not inherit the definition via a property. This is one reason I filed #3714 to try to get to a place where we could make something like a named declaration, and export/import that to/from various places, ie importing rulesets into JS, or importing a mixin into another stylesheet. The idea from #3714 applied to mixins might look something like:
@mixin $center {
display: grid;
place-content: center;
}
// assuming we could add `{name} from` support to `@import` syntax
@import {$center} from url("./utils.css");
body {
@apply $center;
/*
display: grid;
place-content: center;
*/
} This would act a bit more like preprocessor mixins as well, and play nicely with static analyzers, minifiers, etc. |
@mirisuzanne this is awesome! My first big question about mixins and Also, one of the use cases for the older |
They avoid the
Yup, same as in Sass/etc.
No. |
Coming to this quite late (I only discovered it yesterday from @mirisuzanne’s mention in #8738). Sharing some thoughts below. Commonalties between mixins and functionsI’m not sure about defining functions and mixins together. They have fundamentally different use cases, and I’m worried defining them together could hold one of them back from its full potential.
I'd suggest the opposite process: iterate on them separately then find the common concepts and spec those together at the end. Use casesFor both mixins and functions, there are two fundamental use cases, and we need to ensure both are served well:
These are a spectrum. One may start with a local mixin/function, then decide that they are more broadly useful. An observation is that mixins also facilitate encapsulation. Today if you have multiple classes that share styling, either you need to alter the structure of your CSS code, OR just ask people to use both the base class and the more specific classes (e.g. RequirementsMust-have, even in the MVP
Must-have, but we could be left out in L1 if really needed
Important but not essential
SyntaxMixinsThe current syntax proposed uses a Using parameters in conditional rulesThe proposal asserts that we could not use Function return value: @-rule or descrptor?
It’s more of a property than an Yes, a generic @-rule is more distinguishable than a descriptor, but that is a syntax highlighting problem, not a language problem. Being a descriptor allows it to cascade (e.g. to provide alternatives based on browser support — though the utility of that is limited due to Actually, come to think of it, do we even need a dedicated descriptor? What if the result was held by a custom property, and which one was part of the function definition: @function --at-breakpoints(
--s: 1em;
--m: 1em + 0.5vw;
--l: 1.2em + 1vw;
) returns --result {
@container (inline-size < 20em) {
--result: calc(var(--s));
}
@container (20em < inline-size < 50em) {
--result: calc(var(--m));
}
@container (50em < inline-size) {
--result: calc(var(--l));
}
} And I like @FremyCompany’s idea as a default: if Oh, or what if what it returns is actually a proper value? @function --hypot(--a, --b) returns var(--result) {
--result: calc(sqrt(pow(var(--a), 2) + pow(var(--b), 2));
} Then you can make the braces optional and have a shortform function definition: @function --hypot(--a, --b) returns calc(sqrt(pow(var(--a), 2) + pow(var(--b), 2)); QuestionsDo mixins cascade down Shadow DOM boundaries? If so, they may help fix the issue of styling Shadow DOM (e.g. having form controls that are styled like the surrounding page with reasonable DX). |
Great feedback - I've already started working some similar adjustments into the explainer. You can open additional issues if you want for individual aspects, but I don't consider this issue specific to the initial syntax - happy to keep iterating on that. I also don't think mixins/functions should get too tangled. But it did feel useful to explore them together for this initial write-up. That has already helped point to where they are distinct (as you mentioned) and what they seem to share:
But at this point, those all look like things that should be pretty portable. If we get them right in one feature, we should be able to reuse them for the other. |
@LeaVerou Do you just mean something like this, or do you have something more advanced in mind?
|
A random idea. What if it were allowed to apply a mixin using
Looks almost like a custom property =) |
Mixins aren't properties, tho. And adding a brand new syntax needs some significant justification. |
This isn't true, tho. What you're describing is usually called "dynamic scope", where a function can get access to the values of variables in the context it was called. Most languages use "lexical scope", where a function has access to variables in the context it was defined, and then when it's called the only additional information comes from the arguments themselves. Dynamic scope is very rare these days, as it's both easy to cause weird errors and harder to optimize. Instead we just pass arguments. (And CSS variables don't exist in the global context in which @function is executed to define the function, so it wouldn't have lexical access to anything.) Giving functions access to outside variables would also make it more difficult to work with functions generically - every outside variable it references is effectively an extra argument, and one that gets passed implicitly without having to know anything is happening. That means you can't name your custom properties arbitrarily, since a function you want to call might also use that property for its own purposes. (The argument is different for mixins, which definitely swizzle your local "state" already by mixing in more properties. Nothing wrong with allowing them the ability to output styles that refer to outside variables.)
By this do you mean letting a mixin use nesting to target styles at other elements? Then yeah, absolutely.
Only insofar as these rules already work when nested inside of style rules. When they do, they should work in mixins; when they don't, they shouldn't.
I don't see what benefit we gain here from using a (configurable?) custom property here. Could you elaborate on why this is better than just using
Right, it can't cascade in that way. We have no idea what the return value is - that requires contextual information about exactly how and where it's used, and we don't do that for typed custom properties, so we don't do that here either. The best we can do is know what type it's meant to resolve to, and verify that it does, so DevTools can complain.
This doesn't require it to be a descriptor. We just need to know the expected type somehow.
The syntax already allows essentially this:
This conflicts very heavily with some core concepts, unfortunately. If you have to do selector matching to even know that a mixin is available, that makes it much more difficult to then apply the mixin, and have it interact with the cascade properly. Disallowing @Mixins from being defined inside of style rules is, luckily, the default case - they're not allowed by the Nesting spec unless we say so. ^_^ |
Lea only listed this as a requirement for mixins, so I don't think there's any disagreement here.
This gets to the question of top-level mixins, which are a fairly common pattern in Sass. I regularly use mixins to generate code (like font rules and keyframes) at the root of the CSS document, not in any nested context. I'm not sure how that would work with a nesting-driven approach to mixins, but there is certainly a use-case for it. On the other hand, it's a use-case that may not have any advantages running on the client. And it's most often used to access flow control and loops, which we don't have at this point. |
I have some opinions about syntax – I don't see any gain from customizing the name of a return-value descriptor, or forcing it to match the function name – but for the most part my goal with this issue was to get confirmation that we want to pursue something along these lines. Functionally, I don't see a big difference between |
@mirisuzanne I was reading back over some updates in your official proposal under the "Other result syntaxes for functions" section. While I see the value in using a property-like keyword like A special function-definition-specific
If specificity plays no role in functions, then we can forget my concern here, but if it does, I think there may be some reasoning left around how we handle situations where users want a certain condition bearing lower specificity to take precedence over a condition of higher specificity. Thanks for all your work on this! |
Since functions exist inside the value space of a property:
Both the function-definition (if multiple rules define the same function name) and function-call (the property where the function is used) will participate in aspects of the cascade – but there really is no way for function internals to impact that. Technically |
Agenda+ to do an intro session for the spec at TPAC |
The CSS Working Group just discussed The full IRC log of that discussion<emeyer> TabAtkins: We agreed to take on functions & mixins specification<emeyer> …At this point, we have a first-draft spec for functions, but not mixins <emeyer> …Not looking for resolutions, just a quick runthrough <emeyer> …Custom functions are defined using an @function rule with a double-dashed identifier <emeyer> …Something it resolved, a value is passed in, calculations are done, the result is passed out <emeyer> …The full syntax is a little larger <emeyer> …IN addition to a parameter list, there’s a dependency list <emeyer> …Particularly from Lea’s feedback, it’s common to want to use defined system variables <emeyer> …Having to pass them to every function is frustrating <emeyer> …Functions can declare they will implicitly pull in extra variables <emeyer> …Can also declare its return type <emeyer> …If the custom function resolves to the wrong thing, we can detect that and act on it <emeyer> …After the fact, if we see something wrong, we invalidate <emeyer> …at use time <lea> q+ <emeyer> …Just like in the other descriptor, if there are multiple results, the last one is what resolves to the value <emeyer> …Conditional rules are permitted <emeyer> …Things like media queries can go inside <kbabbitt> draft is at https://drafts.csswg.org/css-mixins-1/ <emeyer> florian: Local variables, order doesn’t matter? <emeyer> TabAtkins: Correct, they’re orderless <emeyer> florian: I suspect this is correct, but is likely to confuse <emilio> q+ <emeyer> TabAtkins: We hope that the fact it looks like regular styles will guide people to the right behavior <emeyer> lea: SSometimes you need to access certain contexts, but the grammar looks to me like it’s easier to pass in by using <emeyer> TabAtkins: You don’t pass in anything that way <emeyer> …They’re automatically added to arguments at the call site <emeyer> lea: It might be useful to have an aggregate syntax <emeyer> TabAtkins: Please open an issue for that <emeyer> s/SSometimes/Sometimes/ <astearns> ack lea <emeyer> TabAtkins: We have a return type for the function itself <lea> q+ <emeyer> …You can also declare grammars for individual arguments <emeyer> …If you call with the wrong thing, it will invalidate the functoin <emeyer> …There’s a syntax that looks like CSS grammar <florian> q+ to ask what happens if you have multiple return descriptors and only one of them matches the declared type <emeyer> …Authors don’t like wrapping in a string <astearns> q+ <emeyer> …Basically, the grammar looks like custom property registrations <emeyer> …There’s no change in functionality, but you don’t need to wrap in quotes <lea> q+ to ask about system colors (canvas, canvastext) and currentcolor, relative units <emeyer> …So you declare functions, they can use conditional queries <emeyer> …If you rewrite appropriately, this should be equivalent to dropping in a nesting block <Penny> (There is a noise suppression setting on the Zoom control panel set to enabled, but making changes is password protected. We'll need to engage tech support to disable it.) <emeyer> …Questions? <emeyer> emilio: This is a descriptor, not a property? <emeyer> TabAtkins: Right <emeyer> emilio: We need to sort out how this operates on the CSSOM <emeyer> TabAtkins: Yeah, there could be some clashes there <emeyer> emilio: This should look a lot like a style rule, right? <emeyer> TabAtkins: Body will be llike a style declaration <emeyer> emilio: That makes implementation... fun, but okay <emeyer> s/llike/like/ <emeyer> lea: Making sure, system colors would resolve based on the color scheme when this is used? <astearns> ack emilio <astearns> ack lea <Zakim> lea, you wanted to ask about system colors (canvas, canvastext) and currentcolor, relative units <emeyer> TabAtkins: Yes, they’re based on the element the function is applied to <matthieud> q+ <emeyer> …Because of the clash between functions and wider variables, vars in the body are references to (missed by scribe) <astearns> ack florian <Zakim> florian, you wanted to ask what happens if you have multiple return descriptors and only one of them matches the declared type <lea> q+ to say the current grammar for using allows declaring types, is that intentional? <emeyer> florian: If you have declared integer type and multiple returns, do you check them all against type and return the last valid or only check the last one returned against the type? <emeyer> TabAtkins: I’m not sure, but I think we have flexibility <fantasai> I think we want to match the type. <emeyer> …We want to be consistent with other things so probably the second, but we’re not locked in either way <emeyer> fantasai: I think we should take the last one that does match the type <romain> +1 <emeyer> …They might try to do forward opt-in, which would break if we don’t do it that way <kbabbitt> +1 fantasai <astearns> ack astearns <emeyer> astearns: The new non-string representation will also be available for custom property registration? <emeyer> TabAtkins: Yes <astearns> ack matthieud <emeyer> matthieud: Can you only define functions at the root? <emeyer> …And if it’s allowed, (missed by scribe) <emeyer> TabAtkins: They are global and we don’t have a way to store the function and use it elsewhere <astearns> s/(missed by scribe)/(something about closures) <emeyer> lea: If they’re global only, we should not allow them to nest so we have options later <emeyer> …Same for mixins, where scope is valuable <fantasai> s/(missed by scribe)/can we do closures/ <astearns> ack lea <Zakim> lea, you wanted to say the current grammar for using allows declaring types, is that intentional? <emeyer> …THe using grammar allows types, is that intentional? <emeyer> TabAtkins: Yes <emeyer> lea: How does that work if it’s a registered variable? <emeyer> TabAtkins: It still has the token representation <emeyer> lea: So you could recast to a different type <emeyer> TabAtkins: You can already do that by subbing <keithamus> q? <keithamus> q+ <miriam> q+ <astearns> ack keithamus <emeyer> keithamus: Curious how these translate across shadow boundaries <fantasai> s/subbing/subbing a registered variable into an unregistered variable and re-subbing into a differently-registered variable/ <emeyer> TabAtkins: It’s a great question <emeyer> …Been thinking how to expose shadow DOM, and the answer should work but what that means exactly is undefined <emeyer> …BY the time this ships we should have a reasonable answer <astearns> ack miriam <emeyer> miriam: Are we saying the types in the parameter list are not just for validation, they’re actually setting the type of what comes in? <emeyer> TabAtkins: No, they just validate, they don’t otherwise change behavior <emeyer> …We don’t have a way to trigger animations from within a function <emeyer> astearns: Any other questions? <emeyer> …I’m assuming this mostly spec noodling <emeyer> TabAtkins: Right, nothing is imminent <emeyer> astearns: Are there any resolutions you need? <emeyer> TabAtkins: No |
@LeaVerou said:
I don't think we can do that. I don't think can disallow nesting and decide later. |
It's not obvious to me that a scoped condition is the same as nesting. Already we have rules like |
@romainmenke What I was talking about was |
Yeah, I was thinking of wrapping in any rule, including things like |
Two quick questions...
--args: 1, 2, 3;
--val: --custom-fn(var(--args)); /* called with 3 arguments? */
if yes on q 1, (and helped along with a 'yes' on q 2) this opens the door to arrays which means CSS libraries (with minor restrictions) can begin providing shorthand properties in their API! That's gonna be sick! @function --at-0 (--p0) { result: var(--p0); }
@function --at-1 (--p0, --p1) { result: var(--p1); }
@function --at-2 (--p0, --p1, --p2) { result: var(--p2); }
...
.library-border {
--size: --at-0(var(--api-shorthand));
--style: --at-1(var(--api-shorthand));
--color: --at-2(var(--api-shorthand));
border: var(--size, var(--api-size, 1px))
var(--style, var(--api-style, solid))
var(--color, var(--api-color, black));
} <div class="library-border" style="--api-shorthand: 2px, solid, hotpink;">
I have a 2px solid border that's hotpink!
</div> |
Those are interesting questions. I think it would be good to open separate issues for them at this point. I don't have an immediate answer, but would be interested in the discussions/implications. |
Per current specs, yes. (And I think that's good.) If you want to make sure that a variable only expands into one argument, you can call the function like
Yeah, we should raise this as a separate issue, as there are multiple possible answers and none of them are "obvious". |
It's for this reason that Less started leaning towards separation of args & params with semi-colons, and one of the major risks of bringing "function-like" constructs to CSS (which I still feel is not needed, especially not before worklets would be expanded to allow custom functions defined in JS/WASM). Once you start trying to merge the two, you find that, unlike typical programming languages, CSS has many many syntaxes to express the concept of lists. Some lists are space-separated, some are comma-separated, and for some reason, newer & revised color functions use a combination of space and slash-separated. Declaration lists are semi-colon separated, except when they aren't (such as when inter-mixed with child at-rules). When it comes to mixins and functions, then, you run into a conceptual ambiguity. What did the user actually mean? Often times, a comma-separated list in CSS still represents a single conceptual "value". For example, imagine a user writes (forgive me if this isn't the current proposed syntax): // file1.css
@function --custom-font-fn(--fonts) { result: var(--fonts); }
// file2.css
.rule {
font-family: --custom-font-fn(Arial, sans-serif);
} In this case, a developer coming along may assume 1 of 2 things: either the person writing the function definition made a mistake, or the person writing the call made a mistake. Clearly the function requires one value, and that value is clearly a list of one or more font family names... except it isn't clear. It's not intuitive, syntactically, what the output will be. Will Less resolved, in discussions, that the construct that was less ambiguous in CSS is semi-colon separation. In every other case, the "value" sent to a mixin / function just felt ambiguous. For the Less team, the clearest seemed to be: ideally, mixins / functions should have always been called with semi-colon separators between arguments. But, for backwards-compatibility, they could be used instead of commas to reduce ambiguity. So, when semi-colons are forced, then it's clear where they "slot" in the call: // it's much clearer this is not a "font family list value", but two clearly separated values,
// since CSS can't assign a value with semi-colon separators to a property
output: --custom-font-fn(Arial; sans-serif); As an alternative, Less combines the concept of "escaping" with parens, so it allows a single list value like: output: --custom-font-fn(~(Arial, sans-serif)); It looks like the proposal is doing something similar, but with braces? e.g. Or at least I'm inferring that from:
So this? output: --custom-font-fn({ Arial, sans-serif }); For me, braces around a comma-separated list seems non-CSS-y to me, since we don't see that elsewhere, but I guess either way there's some net new (unfortunately) in how CSS defines a list. It seems like either way we'll add another "list definition syntax" to CSS, which I wish wasn't the case, since there's already so many. But anyway, just thought I'd share how other people have approached this issue, in case that's helpful! |
@mirisuzanne @tabatkins thank you both! I've opened the issue @matthew-dean it's a fair assumption for most devs that it would be one argument looking at it from outside of normal-for-css behavior; might want to bring it up again in the new thread - though I do hope it remains CSS-y and spreads to multiple |
It's probably worth its own breakout thread / discussion for CSS to define, in spec, just what a list is, and how / when it's delimited in one way vs. another. As far as I know, there's no syntactic explanation; CSS is mostly a collection of micro-syntaxes that mostly conform to another, and depending on when a property / value set was introduced, can have slightly different syntactic conventions. So there are rough intuitive understandings of when something is a space-separated vs. comma-separated vs. slash-separated list vs. semi-colon separated, but unless I've missed it in the time I've spent with CSS specs, there's not really a theory of the syntax that really explains it... and then on the "function-like" side of things, the semantics of comma-separated lists (distinct argument values) is ever-so-slightly different from the semantics of comma-separated lists in properties (a single, comma-separated value), except when they aren't (e.g. modern color syntax). Space-separated lists tend to be reserved for shorthand values, except when they aren't (e.g. modern color syntax). Case in point, the modern color module spec provides no explanation that I can see for why the syntax changed, what the motivation is/was, or, most importantly, what CSS currently considers to be an "argument" for a "function", or what it considers to be a list. The reason I bring this up is that mixins / functions are ultimately built upon this CSS list ambiguity—CSS has no clear syntactic definition of a list, or when to use one list "type" over another. With defined functions, that's sort of alright, because CSS is full of micro-syntaxes, each function can essentially define it's own syntax. But with custom mixins and functions, you're suddenly painted into a corner. There is no "intuitive" way to define mixins / functions in CSS that is going to make sense to all developers, because CSS has no "generic" function argument list rules. So, IMO, I feel like this is missing a very fundamental piece of discussion, which is to clarify, in CSS specs, what the list types are and what their intended use cases are. If the modern color syntax follows a new syntactic theory of lists, what is it? Why are some "arguments" separated by spaces, and some by slashes? (By the way, it's not just about those color functions -- there are other CSS function and list inconsistencies; its just the easiest go-to example.) If CSS is going to essentially expand itself as having programmatic constructs, then I strongly believe it needs to more clearly define its syntax, specifically around lists. Without it, I don't believe the At the very least, I think CSS would best be served by chunking this work into these parts:
|
Just to provide another example where lists are inconsistently-defined in CSS, let's take Custom Properties. The
The spec goes on to explain:
Internally, it's fine. Once you know how Which then brings us back to something like: So now, it's yet another inconsistency added to other CSS list inconsistencies. That is, there's the notion of coalescing a comma-separated value into a single one... except, if that were a defined concept in CSS, then you would presumably see it in Once the formal definition for CSS lists is complete, two things are going to immediately become easier:
Side note: this doesn't even touch on the fact that sometimes a comma in CSS is not semantically a separator between distinct values but is an operator, such as as an |
Background
Read the full explainer
There's an existing issue for Declarative custom functions, which forms the basis for much of this proposal. I'm opening a separate issue here, in order to make a broader proposal building on that, which includes both functions and mixins.
From a language perspective, mixins and functions are somewhat distinct – they live at different levels of the syntax, and come with different complications. It may make sense to handle them in different levels of a specification (likely functions first) or even different specifications altogether. Function-specific discussion could move back to the existing thread for that work. However, the features also have a lot in common from the author/syntax perspective, so I wanted to consider them together here, without cluttering the main thread.
Intro
Both Mixins and Functions provide a way to capture and reuse some amount of logic. That can be used for the sake of developer shorthands, and also as a way of ensuring maintainability by avoiding repetition and encouraging consistent use of best practice patterns. For many years, authors have been using pre-processors to perform this sort of CSS abstraction – or experimenting with custom property tricks like the space toggle hack, and recently style queries. There's also an open issue for Higher level custom properties with many mixin-like use-cases.
By providing a native CSS solution for these use-cases, we can help simplify web tooling/dependency requirements – while also providing access to new functionality. Mixins and functions in the browser should be able to accept custom property arguments, and respond to client-side media, container, and support conditions.
The overlapping syntax basics
Both functions and mixins need to be defined with a (custom-ident)
name
, aparameter-list
, some amount of built-in-logic, and someoutput
to return. The difference between the two is where they can be used in CSS, based on the type of output they provide:For the basics, I'm proposing two new at-rules following a similar pattern:
The parameter lists should be able to define parameters with a (required)
name
, an (optional)default
, and potentially (optional)<syntax>
. In order to allow custom-property values with commas inside, we likely need a;
delimiter both in defining and passing arguments, where values are involved. To re-use existing custom-property syntax, we could do something like:[Edited] @emilio has suggested potentially having parameter names only in the parameter list, and then
@parameter
-like rules in the body of the function/mixin when default values or syntax descriptor are needed. That would remove the need for;
delimiters in the prelude entirely. I'm not attached to all the details of the syntax here, but borrowed from existing structures. If we don't need thesyntax
definition for parameters, or can add that later, it might allow us to simplify further.Internally, both syntaxes should allow conditional at-rules such as
@media
,@container
, and@supports
. That's one of the primary functions that CSS-based functions and mixins could provide. I don't think non-conditional or name-defining at-rules would serve any purpose, and should likely be discarded.Functions
Normal properties inside a function would have no use, and could be discarded and ignored. However, it would be useful for functions to have internally-scoped custom properties. To avoid accidental conflicts, internal function logic would not have access to external custom properties besides the values explicitly passed in to the defined parameters.
In addition to allowing (scoped) custom properties and conditional at-rules, a function would need to define one or more resulting values to return. I like the at-rule (e.g.
@return
) syntax suggested in the original thread, though theresult
descriptor could also work. If more than one value would be returned, the final one should be used (to match the established last-takes-precedence rules of the CSS cascade).An example function with some conditional logic:
Functions would be resolved during variable substitution, and the resulting computed values would inherit (the same as custom properties).
Mixins
Mixins, on the other hand, will mostly contain CSS declarations and nested rules that can be output directly:
I don't believe there is any need for an explicit
@return
(though we could provide one if necessary). Instead, if there is any use for mixin-scoped or 'private' custom properties, we could consider a way to mark those specifically. Maybe a flag like!private
would be enough?Another possible example, for gradient text using background-clip when supported:
There are still many issues to be resolved here, and some syntax that should go through further bike-shed revisions. Read the full explainer for some further notes, including a suggestion from @astearns for eventually providing a builtin keyframe-access mixin to help address the responsive typography interpolation use-case.
The text was updated successfully, but these errors were encountered: