Skip to content
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

[css-variables-2] Custom shorthands with @property #7879

Open
romainmenke opened this issue Oct 13, 2022 · 26 comments
Open

[css-variables-2] Custom shorthands with @property #7879

romainmenke opened this issue Oct 13, 2022 · 26 comments

Comments

@romainmenke
Copy link
Member

romainmenke commented Oct 13, 2022

Context :

Currently there is no way to reuse small groups of declarations (keys and values) in multiple styles without creating a style rule for them.

.want-to-reuse {
	color: hotpink;
	padding: 15px;
	display: flex;
}

To apply this to multiple components you need to alter your markup.
Or you need to copy/paste the declarations to each style rule.


Mixins are often used to work around this.
I know mixins will never be a thing in native CSS and maybe the same reasons behind that apply to this proposal.

@mixin some-mixin {
	color: hotpink;
	padding: 15px;
	display: flex;
}

selector {
	@include some-mixin;
}

Custom properties also help to some extend because they allow values to be re-used.
Naming conventions help to group sets of values.

:root {
	--some-color: purple;
	--some-padding: 20px;
	--some-display: flex;
	
	--fancy-color: hotpink;
	--fancy-padding: 15px;
	--fancy-display: flex;
}

Proposal :

Can custom shorthands be defined through @property?

  • syntax must match the declared constituent properties
  • keywords like initial work exactly like they work for native shorthands

I included padding in the example but maybe shorthand in shorthands have issues?

@property --some-shorthand {
	syntax: "<color>" "<length>" "<ident>";
	constituent-properties: color padding display;
	values:
		default / purple 20px flex,
		fancy / hotpink 15px flex;
}

.foo {
	--some-shorthand: fancy;
}

Is equivalent to :

.foo {
	color: hotpink;
	padding: 15px;
	display: flex;
}

Set values of constituent properties :

.foo {
	--some-shorthand: green 10px block;
}

This is somewhat related to #7273 as it allows similar patterns when styling elements.

@romainmenke
Copy link
Member Author

Nested shorthands could work with something similar as the grid syntax : https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas

@property --some-shorthand {
	syntax: "<color>"
		"<text-decoration-line> <text-decoration-style> <text-decoration-color>"
		"<ident>";
	constituent-properties: color text-decoration display;
	values: ...;
}

@romainmenke
Copy link
Member Author

romainmenke commented Oct 14, 2022

/ is a bit ambiguous with ratio's (aspect-ratio: 1/2;)
But maybe this is fine

<ident> / <any-value>+

vs.

<number> / <number>

The only purpose of the / between the value set name and the value list is to visually separate these.

Maybe better to omit this and have more flexibility.

@ariarzer
Copy link

I very like that idea 😍
Even more because it is strongly typed.

But there is one thing I don't like.
This code...

--some-shorthand: green 10px block;

... looks like definition, not like applying.

I like the way how it's made in Less. With an @includes property with a list of "mixins" names in the value. It looks more like 'apply this mixims to this block' than 'there i define a new value for this mixin'.

I don't know if CSS let property be named from the symbol @, and do it need to be called includes, but i think there needs at least a new property for it)

@romainmenke
Copy link
Member Author

I remember seeing in multiple places that @tabatkins mentioned that mixins, @include and @apply will never be a thing.

I however could not find the original reasoning behind it.
So I am unsure if this was related to the syntax, some aspect of those other features or if it also covers this proposal.


looks like definition, not like applying.

You are declaring values for properties.
It is the exact same syntax as it is for things like text-decoration.

The single keyword shortcuts are more unlike css I think.

@LeaVerou
Copy link
Member

LeaVerou commented Oct 14, 2022

I really like the idea of emulating mixins by being able to define custom shorthands. I also really like being able to define values for these shorthands that expand to completely separate values in the longhands, though maybe that can be left to conditionals?

However, defining the syntax of the shorthand like this could be problematic as it can create disambiguation issues that are not present in the individual shorthands. E.g. consider property a with a syntax <length>{0,4} and property b with a syntax <length>{0,4}. If we define:

@property --some-shorthand {
	syntax: "<length>{0,4}" "<length>{0,4}";
	constituent-properties: a, b;
}

Then what is --some-shorthand: 1px 2px 3px 4px setting?

It is also quite a burden on authors to have to match CSS property syntax. What if they don't need the entire possible range of syntaxes? It is also an impossible feat: even if they matched it when they wrote the rule, what happens when the syntax changes?

Additionally, most CSS properties have much more complex syntaxes that what can be defined with the syntax descriptor, and it's not ideal if this mechanism had to exclude them. Even after syntax evolves more, I doubt it will ever be able to cover the entire range of microsyntaxes we have in CSS, even our actual grammar syntax doesn't cover it and sometimes we have to use prose!

I think instead we should have syntax that somehow destructures the shorthand value into constituent custom properties that we then feed into CSS properties in the definition, without the author having the burden of duplicating existing CSS property syntax. Something like (super handwavy sketch):

@shorthand --some-shorthand {
	/* Read <length> into --some-shorthand-padding 
	   and <color> into --some-shorthand-color */
	syntax: padding "<length>", color "<color>";

	@expands-to {
		padding: calc(10px + var(--some-shorthand-padding, 0);
		color: var(--some-shorthand-color, black);
		border: 1px solid var(--some-shorthand-color, transparent);
	}
}

or even, with Nesting (of course then it's no longer a shorthand, but more of a mixin):

@mixin --some-shorthand {
	/* Read <length> into --some-shorthand-padding 
	   and <color> into --some-shorthand-color */
	syntax: padding "<length>", color "<color>";

	@expands-to {
		padding: calc(10px + var(--some-shorthand-padding, 0);
		color: var(--some-shorthand-color, black);
		border: 1px solid var(--some-shorthand-color, transparent);

		&:hover { 
			color: lighter(var(--some-shorthand-color, black));			
		}
	}
}

@LeaVerou
Copy link
Member

I remember seeing in multiple places that @tabatkins mentioned that mixins, @include and @apply will never be a thing.

I however could not find the original reasoning behind it. So I am unsure if this was related to the syntax, some aspect of those other features or if it also covers this proposal.

What I remember was that @apply was dropped because its scoping didn't make sense. @tabatkins, can you shed some light?

@romainmenke
Copy link
Member Author

romainmenke commented Oct 14, 2022

However, defining the syntax of the shorthand like this could be problematic as it can create disambiguation issues that are not present in the individual shorthands. E.g. consider property a with a syntax {0,4} and property b with a syntax {0,4}.

Agreed.

The only way to make it not ambiguous is by not allowing multipliers in the syntax definitions. This creates a fixed number of values that can be declared. (ignoring multi-token values like <ratio> for now)

We trade in some flexibility and gain some composability.
Authors have control over how far they want to take it.

This is typical for such patterns also in other languages and contexts.
By encapsulated a thing you make it easier to use but it is also more defined, more restricted.


It is also quite a burden on authors to have to match CSS property syntax. What if they don't need the entire possible range of syntaxes? It is also an impossible feat: even if they matched it when they wrote the rule, what happens when the syntax changes?

Being able to reference properties might help, but this again opens up the door to ambiguous value lists when a shorthand property is referenced.

  • <'color'> vs <color>
  • <'padding'> vs <length>

Aside from that I think this is mostly fine.
If the syntax defined in the custom shorthand doesn't allow your preferred value, you can use the original property yourself.

Being able to pass individual values to the shorthand is mostly an escape hatch.
It can even be stripped entirely from this proposal.

But I do think it enables some nice patterns.
(assigning a value list to another custom property to create your own "presets")


I think instead we should have syntax that somehow destructures the shorthand value into constituent custom properties that we then feed into CSS properties in the definition, without the author having the burden of duplicating existing CSS property syntax.

That is actually exactly what this proposal does, minus that last bit.
Following what already exists for @property made the most sense to me.

This also has the benefit of making these fully self documenting allowing editors and other tools to provide auto complete, linting, hover text, ...


The main feature I think is the values property.
It is the "public surface" of the "API".

I expect most users of a defined custom shorthand to just use one of the "preset" value lists.

@romainmenke
Copy link
Member Author

romainmenke commented Oct 14, 2022

The only way to make it not ambiguous is by not allowing multipliers in the syntax definitions. This creates a fixed number of values that can be declared. (ignoring multi-token values like for now)

This might be problematic given that + and # are currently included in the specification.

It is confusing that different syntax definitions would be allowed between longhand and shorthand.

@Loirooriol
Copy link
Contributor

I have several doubts about this, shorthands are not as simple as they may seem at first glance.

From my experience of implementing properties, shorthands need 3 parts:

  1. The list of longhands
  2. The logic for expanding it into longhands
  3. The logic for serializing it from the longhands (and for computed values it may be different than for specified values!)

It's not clear to me that these are all covered in a non-ambiguous way.

Taking Lea's example:

@shorthand --some-shorthand {
	/* Read <length> into --some-shorthand-padding 
	   and <color> into --some-shorthand-color */
	syntax: padding "<length>", color "<color>";

	@expands-to {
		padding: calc(10px + var(--some-shorthand-padding, 0);
		color: var(--some-shorthand-color, black);
		border: 1px solid var(--some-shorthand-color, transparent);
	}
}

Does this define --some-shorthand-padding with syntax: "<length>" and --some-shorthand-color with syntax: "<color>"? Or would they need to be defined separately, defaulting to syntax: "*" if not?

If the shorthand defines the longhands, then what defines the initial-value and inherit descriptors of each longhand? If the shorthand doesn't define the longhands, their syntax may allow arbitrary values, making serialization more complex.

Also, I fail to see how the @expands-to is related to the shorthand. How is it different than this?

@shorthand --some-shorthand {
	/* Read <length> into --some-shorthand-padding 
	   and <color> into --some-shorthand-color */
	syntax: padding "<length>", color "<color>";
}
*, * :>> * {
	padding: calc(10px + var(--some-shorthand-padding, 0);
	color: var(--some-shorthand-color, black);
	border: 1px solid var(--some-shorthand-color, transparent);
}

The only way to make it not ambiguous is by not allowing multipliers in the syntax definitions

And | is also problematic. And ? would be problematic if syntax accepted it. Etc.

@LeaVerou
Copy link
Member

LeaVerou commented Oct 14, 2022

The logic for serializing it from the longhands

But not all longhands can be serialized as shorthands anyway.

Does this define --some-shorthand-padding with syntax: "<length>" and --some-shorthand-color with syntax: "<color>"? Or would they need to be defined separately, defaulting to syntax: "*" if not?

Both, plus --some-shorthand with syntax <length> <color>.

How is it different than this?

padding: 1px;
--some-shorthand: 10px black;

would give you a different result.

@Loirooriol
Copy link
Contributor

padding: 1px; --some-shorthand: 10px black; would give you a different result.

OK, then padding-*, color and border-* should be longhands of --some-shorthand.

And creating shorthands of standard properties seems problematic, because they can have a complex grammar.

@romainmenke
Copy link
Member Author

romainmenke commented Oct 15, 2022

The logic for expanding it into longhands

The core of this proposal is a simple system of "slots".

  • 3 properties (3 times "anything")
  • 2nd property takes 3 values
syntax: "<color>"
	"<text-decoration-line> <text-decoration-style> <text-decoration-color>"
	"<ident>";

Equivalent to :

syntax: "a"
	"b b b"
	"c";

If this system is in any way ambiguous it can not be implemented.
So it is correct that |, ?, * are also problematic.

| would be ok with more rules (i.e. each part must have the same number of terms).

  • yes : <color> | [ red | blue ]
  • no : <color> | [ red | [ blue 2px ] ]

Avoiding the syntax definitions entirely and using "a" "b b b" "c" literally might be better? Authors already know this format from grid.

This does not actually remove any type information because the properties and order are known.

@property --some-shorthand {
	syntax: "a"
		"b b b"
		"c";
	constituent-properties: color text-decoration display;
	values: ...;
}
  • a : <'color'>
  • b1 : <'text-decoration-line'> | <'text-decoration-style'> | <'text-decoration-color'>
  • b2 : <'text-decoration-line'> | <'text-decoration-style'> | <'text-decoration-color'>
  • b3 : <'text-decoration-line'> | <'text-decoration-style'> | <'text-decoration-color'>
  • c : <'display'>

Padding itself has this value definition <'padding-top'>{1,4}
But authors would write :

@property --some-shorthand {
	syntax: "a"
		"b b"
	constituent-properties: margin padding;
	values: ...;
}
  • a : <'margin-top'>
  • b1 : <'padding-top'>
  • b2 : <'padding-top'>

Both a and b1 b2 are subsets of the allowed syntaxes for margin and padding.

Users can not deviate from the template defined with syntax but the result is unambiguous and enables the core feature.

@romainmenke
Copy link
Member Author

@LeaVerou said :

It is also quite a burden on authors to have to match CSS property syntax. What if they don't need the entire possible range of syntaxes? It is also an impossible feat: even if they matched it when they wrote the rule, what happens when the syntax changes?

I agree with this more now.

What happens when they make a mistake?
A simple typo or a moment of confusion can invalidate and break a large part of their code base.

A more simple "passthrough" system as I laid out above is better I think.

@romainmenke
Copy link
Member Author

romainmenke commented Oct 15, 2022

@LeaVerou I didn't fully grasp this example before.
Having the ability to encode transforms for the values passed to the shorthand seems really handy and powerful but I am unsure if it is needed.

@mixin --some-shorthand {
	/* Read <length> into --some-shorthand-padding 
	   and <color> into --some-shorthand-color */
	syntax: padding "<length>", color "<color>";

	@expands-to {
		padding: calc(10px + var(--some-shorthand-padding, 0);
		color: var(--some-shorthand-color, black);
		border: 1px solid var(--some-shorthand-color, transparent);

		&:hover { 
			color: lighter(var(--some-shorthand-color, black));			
		}
	}
}

I think you can achieve the same result with this :

@property --some-shorthand-padding {
  syntax: "<length>";
  inherits: false;
  initial-value: 0;
}

@property --some-shorthand-color {
  syntax: "<color>";
  inherits: false;
  initial-value: black;
}

@property --some-shorthand {
	syntax: "a"
		"b";
	constituent-properties: --some-shorthand-padding --some-shorthand-color;
	values: purple-version / 20px purple,
		yellow-version / 30px yellow;
}

/* internal "API" */
.some-element {
	padding: calc(10px + var(--some-shorthand-padding, 0));
	color: var(--some-shorthand-color, black);
	border: 1px solid var(--some-shorthand-color, transparent);
}

/* user writes */
.some-element {
	--some-shorthand: yellow-version;
}

Equivalent to:

.some-element {
	padding: calc(10px + var(--some-shorthand-padding, 0));
	color: var(--some-shorthand-color, black);
	border: 1px solid var(--some-shorthand-color, transparent);
}

/* user writes */
.some-element {
	--some-shorthand-padding: 30px;
	--some-shorthand-color: yellow;
}

I have no idea how handy this is in practice and I would need to cook up some more realistic examples to be able to judge this.

But I think it is really nice that creators of components can have complex internals and expose something as simple as :

.some-element {
	--some-shorthand: yellow-version;
}

@LeaVerou
Copy link
Member

As I replied to @Loirooriol earlier, this:

sufficiently general selector {
	padding: var(--foo);
}

.foo {
	--foo: 10px;
}

Is not equivalent to:

.foo {
	--shorthand-with-padding: 10px;
}

In the former, if anything, even with the tiniest specificity overrides padding, no amount of setting --foo to anything will affect the padding, whereas in the latter, the declaration will expand to something that includes padding, and can override other padding declarations.

@romainmenke
Copy link
Member Author

romainmenke commented Oct 15, 2022

In the former, if anything, even with the tiniest specificity overrides padding, no amount of setting --foo to anything will affect the padding.

I fail to see the issue with that.
Maybe the example given is too terse and I am missing the point.

But as I understand it we have sufficient tools with specificity, cascade layers and !important to control which properties can be overridden and what the final result will be.

@Loirooriol
Copy link
Contributor

I think this can be considered as 2 different proposals:

  • Being able to define value aliases for a custom property
    So e.g. you can have a custom property --button-text-color with syntax <color>, but then define the aliases --normal = #000 and --disabled = #6d6d6d. So then you can have things like

    button:disabled { --button-text-color: --disabled }
    button.disabled { --button-text-color: --disabled }

    if for some reason you want to avoid a selector list, without having to repeat or remember #6d6d6d.
    This would be similar to --button-text-color: var(--disabled) with --disabled: #6d6d6d, but without exposing --disabled to all properties.
    Aliases should probably not be allowed on properties with syntax *, they should be dashed idents to avoid ambiguities (e.g. <color> accepts idents), and they shouldn't be combined with other values.

  • Being able to define a custom property as a basic shorthand
    Just redirecting the provided values to different properties. No transformation of values nor complex things. The longhands should probably be custom properties too.

And then you could use them together.

@Loirooriol
Copy link
Contributor

Just realized there is a big problem with shorthands: they must expand into longhands at parse time. But @property doesn't affect the parsing, as explained in https://drafts.css-houdini.org/css-properties-values-api/#parsing-custom-properties

@romainmenke
Copy link
Member Author

romainmenke commented Oct 15, 2022

@Loirooriol Do you see another way this feature is implementable?

I don't think it solves enough author cases to warrant entirely new grammar/syntax that enables expanding at parse time.

Especially since it is just syntactic sugar and it can be a preprocessor only thing.

@Loirooriol
Copy link
Contributor

Maybe the expansion could be deferred until computed value time, though then there can be a conflict if there are declarations for the longhands

@romainmenke
Copy link
Member Author

I don't think that is worth it.

I think that rules out using @property and the custom property syntax (--foo) as a way to implement custom shorthands.

@tabatkins
Copy link
Member

I remember seeing in multiple places that @tabatkins mentioned that mixins, @include and @apply will never be a thing.

My argument was specifically against @apply. Things like mixin/include, or this custom shorthand idea, are fine, or at least will be bad for different reasons. ^_^

@romainmenke
Copy link
Member Author

Thank you for that @tabatkins, interesting read!

Also thank you for the feedback and insights @Loirooriol @LeaVerou 🎉
Going to close this because of expected implementation hurdles.

@LeaVerou
Copy link
Member

Going to close this because of expected implementation hurdles.

Was there some kind of discussion outside the repo about implementation hurdles? This does not seem to follow from the last couple of comments.

@romainmenke
Copy link
Member Author

@Loirooriol said :

Just realized there is a big problem with shorthands: they must expand into longhands at parse time. But @Property doesn't affect the parsing, as explained in

But maybe I misunderstood this or the impact of these hurdles.

I am obviously in favour of re-opening to explore this further but I also don't want to take up everyones valuable time with an idea that isn't practical :)

@LeaVerou
Copy link
Member

That just means we can't use @property for this. Though the reasons @property doesn't affect parsing also apply to whatever we do here (namely, that we cannot be reparsing an entire stylesheet every time a rule changes). However, that doesn't mean defining mixins or shorthands is impossible, it's just one problem that needs to be addressed in whatever proposal we come up with.

@romainmenke romainmenke reopened this Oct 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants