w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.35k stars 641 forks source link

[css-values-4] inherit() function: like var() for parent value, for any property #2864

Open LeaVerou opened 6 years ago

LeaVerou commented 6 years ago

We've declined numerous author requests to extend var() to arbitrary properties, due to the potential for cycles and how expensive cycle detection would be in the general case. However, providing a way to get the parent or inherited value of any property does not cause cycles and still helps with a ton of use cases.

Use cases

Custom property value that depends on parent value

Generic --depth (1, 2)

* {
    --depth: calc(inherit(--depth, 0) + 1);
}

See also: https://github.com/w3c/csswg-drafts/issues/1962

Font metrics relative to parent

strong {
    font-weight: clamp(600, 1.2 * inherit(font-weight), 999);
}

And any other numerical typographic metric that can be specified in CSS, e.g. font-stretch, font-style (for variable fonts) etc

Many of the currentBackgroundColor use cases

https://github.com/w3c/csswg-drafts/issues/5292

While this does not address all use cases (since it would only provide an ancestor background color), it does address quite a few, and offers a workaround for others.

Matching nested radii

https://github.com/w3c/csswg-drafts/issues/7707

inherit inherit() + calc
Screen Shot 2022-09-07 at 9 15 06 AM Screen Shot 2022-09-07 at 9 15 11 AM
.child {
    padding: 1em;
    border-radius: calc(inherit(border-radius) - 1em);
}

Swapping foreground and background colors

.button {
    color: inherit(background-color);
    background: inherit(color);
}

Decorations visually extending the background

https://twitter.com/HugoGiraudel/status/1350496044681990147

blockquote::before {
    content: "❝";
    color: inherit(background-color);
    font-size: 300%;
}

Override parent margins (bleed)

.card > header {
    margin-top: calc(-1 * inherit(padding-top));
    margin-left: calc(-1 * inherit(padding-left));
    margin-right: calc(-1 * inherit(padding-right));
}

Inherit grandparent value

https://twitter.com/cjw0/status/1350499207648583683

.foo {
    --font-size: inherit(font-size);
    font-size: 0; /* for animation */
}

.foo > * {
    font-size: var(--font-size);
}

@property does provide a workaround for this particular case (just register a <length> custom property and set it on .foo’s parent), but not in the general case where no suitable registration synstax exists.

More use cases

upsuper commented 6 years ago

We probably don't want it to be a function, or at least not a function accepting arbitrary property. Different properties may have different underlying storage / handling, so it could be hard if it uses arbitrary property in arbitrary property.

It might be fine to restrict it to properties with only a single numeric value, but I believe in the past we occasionally extend single-value properties into multi-values, so a general mechanism like this may not be future-proofing.

upsuper commented 6 years ago

Also it sounds like a dupe of #2764.

jonjohnjohnson commented 6 years ago

@FremyCompany mentioned the cascade proposal and I imagine this playing into that? https://github.com/w3c/csswg-drafts/issues/1594#issuecomment-373241477

LeaVerou commented 6 years ago

@upsuper Not a dupe of #2764, which is about a special case of this.

Could you elaborate a bit on the underlying storage/handling issue? How could it get in the way? Is it theoretical or are there existing examples of properties that would have issues?

Also, by future-proofing, are you referring to implementation issues or author code?

upsuper commented 6 years ago

Could you elaborate a bit on the underlying storage/handling issue? How could it get in the way? Is it theoretical or are there existing examples of properties that would have issues?

I wrote that mostly concerning the implementation difficulties in mind, since we (in Stylo) use lots of different types to handle subtle differences between properties. But I realized that it is actually not a problem of types, but the differences themselves. For example, word-spacing and letter-spacing is almost the same except that the former accepts <percentage> in addition to normal | <length>. If any property can be referenced to any property, we would need a mechanism to extract only certain type of value, with either a fallback or just treating as invalid at computed-value time.

Such mechanism has its own issue, that supported type at a value position may depend on the context, and can change as CSS evolves. For example, there is plan to allow division between two dimensions, which may enable <percentage> being used in a property accepting only <dimension> as far as you divide it with another percentage. Also there is some idea about allowing auto in calc for some properties. Those kind of changes may affect above-mentioned mechanism.

Also, by future-proofing, are you referring to implementation issues or author code?

For spec about how to handle it. Actually the above has something related to future-proofing already.

LeaVerou commented 6 years ago

It doesn't need to retain anything about the type, just serialize the value and use it the same as when var() is used.

upsuper commented 6 years ago

OK, so I misunderstood the approach you are proposing. This approach is probably better to some extent, although I'm a bit concerned that serialization of computed value of properties may not be as well-defined and interoperable as token stream used by custom properties.

tabatkins commented 6 years ago

Discussion with dbaron suggested that the effect would be that you basically just sub in the serialization of the computed value (probably per the well-specified serialization in Typed OM).

If the property you're subbing in doesn't work for the property you're subbing into, it would follow the same rules as var(), and make it invalid at computed-value time.

CyberAP commented 5 years ago

Shouldn't it be called computed() then, instead of inherit()? Makes more sense to me and it is more declarative, than an imperative inherit. Or just simply prop() as it's a direct CSS property reference here.

LeaVerou commented 3 years ago

I just came across this issue again looking for something else and was reminded of how useful this would be.

@tabatkins wrote

Discussion with @dbaron suggested that the effect would be that you basically just sub in the serialization of the computed value (probably per the well-specified serialization in Typed OM).

If the property you're subbing in doesn't work for the property you're subbing into, it would follow the same rules as var(), and make it invalid at computed-value time.

It sounds like there is a clear implementation path, but the discussion just stalled 2 years ago. Can we move it forwards?

I promised in the first post that if this is feasible, I could try and collect a list of use cases. Here are a few:

Edit: Added these to OP, for convenience.

In terms of how it should behave, a few more thoughts:

Loirooriol commented 3 years ago

Does it return the parent value regardless of what it is, or walk up ancestors until it finds a value or hit the root?

In general I don't think walking ancestors makes much sense for non-inherited properties. Even for inherited ones it may be undesirable. So I would just return the parent value.

It should probably have a fallback, just like var(). The fallback would apply when the returned value is initial.

But using the fallback for initial values would be inconsistent with var() and env(). I think the fallback should be used when the serialization of the computed value is the empty string (like a guaranteed-invalid value, or a shorthand that can't represent the values of its longhands). IMO custom behaviors for specific values should be achieved with conditionals (#5624).

Can shorthands be referenced? If so, what do they return?

If this feature is based on serializations, then it doesn't seem problematic to me. Just try to serialize the shorthand, and return that in case of success, or empty string otherwise. When reparsed, empty string can be treated as a guaranteed-invalid value, and use the fallback value if provided, or become IACVT otherwise.

LeaVerou commented 3 years ago

@Loirooriol

Does it return the parent value regardless of what it is, or walk up ancestors until it finds a value or hit the root?

In general I don't think walking ancestors makes much sense for non-inherited properties. Even for inherited ones it may be undesirable. So I would just return the parent value.

I suppose that's more predictable (and probably more performant and easier to implement). After all, if one wants the value from a specific ancestor, there's an easy workaround:

/* Swap background & foreground in descendants use case: */
.container > * {
    --container-background: inherit(background-color);
}

.container * {
    color: var(--container-background);
    background: inherit(color);
}

It should probably have a fallback, just like var(). The fallback would apply when the returned value is initial.

But using the fallback for initial values would be inconsistent with var() and env(). I think the fallback should be used when the serialization of the computed value is the empty string (like a guaranteed-invalid value, or a shorthand that can't represent the values of its longhands). IMO custom behaviors for specific values should be achieved with conditionals (#5624).

The fallback for var(--foo) is applied when --foo is initial ([demo],(https://dabblet.com/gist/b49604259b2765fec0966ba649dad8c8) spec). I agree with your reasoning however, especially since this allows shorthands to be used, which is really important.

env() has an entirely different fallback mechanism (falls back when the name is not recognized, not depending on the value).

Can shorthands be referenced? If so, what do they return?

If this feature is based on serializations, then it doesn't seem problematic to me. Just try to serialize the shorthand, and return that in case of success, or empty string otherwise. When reparsed, empty string can be treated as a guaranteed-invalid value, and use the fallback value if provided, or become IACVT otherwise.

I really like this. 💯

Loirooriol commented 3 years ago

The fallback for var(--foo) is applied when --foo is initial

Ah, yes, that's because the initial value for unregistered custom properties is the guaranteed-invalid value, which is what triggers the fallback. But if you register the property with some other initial value, then initial doesn't trigger the fallback in var(), so I guess it shouldn't in inherit() either.

andruud commented 3 years ago

walk up ancestors until it finds a value

Sounds like it's already settled, but I'll emphasize anyway that this is something we should definitely not do.

sub in the serialization of the computed value

We recently (-ish) invented the equivalent token sequence, so we can just re-use that for this.


I've been trying to think about whether or not the new "cross property inheritance" proposed here creates any significant new problems (performance, complexity, etc) for style recalcs. I think it should be fine. It's similar to situations that must anyway be dealt with via var() and explicit inheritance of non-inherited properties.

This looks like a relatively "easy win", so I'm supportive, FWIW. :+1:

LeaVerou commented 3 years ago

Since there is implementor interest, I'm adding this to the agenda for the next call so we can resolve about work on it.

argyleink commented 3 years ago

Here's a rad use case with color-contrast() from Color 5

p {
  color: color-contrast(
    inherit(background-color) 
    vs 
    var(--brand-1), var(--brand-2), var(--brand-3)
    to AA
  );
}

On brand, dynamic and high contrast text color on 🤷🏻

LeaVerou commented 3 years ago

Thinking about this a bit more, I think it should be called parent() or parent-var(), not inherit() which implies action, not value.

A few more use cases (in addition to those in https://github.com/w3c/csswg-drafts/issues/2864#issuecomment-761713059 ):

getify commented 3 years ago

Here's my use-case:

I regularly will set a CSS variable in a parent element, then use that value for some dimension (like width). Then, in a child element, I'll use that same CSS variable and perform a calc(..) on it to derive a new value for the child element to use for some other dimension (like padding or border width). In this case, I'm only using the CSS variable because of its ability to be inherited down to the child and re-used.

It would be much nicer if, in those cases, I could skip the CSS variable altogether, and just have the child element reference a parent CSS property value, like for example: border-width: calc(parent(width) / 10).

mirisuzanne commented 3 years ago

Working with custom properties, I've wanted this a few times. Here are a few examples (some may duplicate Leas examples above):

.examples {
  /* change a value locally, without losing context */
  --columns: parent(--span);
  --span: 3;
  width: calc(var(--span) / var(--columns) * 100%);

  /* normal properties */
  color: color-contrast(parent(background-color) vs black, white);
  margin: calc(parent(padding) * -1);

  /* more generic patterns */
  --alternate: calc(parent(--alternate) * -1);
  --count: calc(parent(--count) + 1);

  --switch-1: parent(--switch-2);
  --switch-2: parent(--switch-1);
}
LeaVerou commented 3 years ago

@andruud While talking to authors about use cases, it seems that while looking up the parent value addresses many of them, there are use cases where the value of a higher up ancestor is required. Am I right to assume that this is also equally feasible as long as it's a specific ancestor (e.g. 3 levels up) and not a search, and the question is what syntax to best use to specify which ancestor to look at?

css-meeting-bot commented 3 years ago

The CSS Working Group just discussed [css-values-4] inherit() function: like var() for parent value, for any property, and agreed to the following:

The full IRC log of that discussion <Rossen_> Topic: [css-values-4] inherit() function: like var() for parent value, for any property
<Rossen_> github: https://github.com/w3c/csswg-drafts/issues/2864
<dlibby_> leaverou: numerous request to extend var() to arbitrary properties, can't do this generally due to cycles. seems to be a less powerful pattern that may be possible
<dlibby_> leaverou: we can use it to get the value from the parent
<dlibby_> leaverou: should we do work to pursue this? I volunteer if consensus that it is a good idea
<dlibby_> leaverou: I can point to many use cases, in case they are not obvious.
<Rossen_> q?
<Rossen_> ack hober
<dlibby_> emilio: seems implementable, but we want it to work like var() at the token level
<dlibby_> emilio: you need to define for each property how it serializes, etc.. it's a lot of work
<dlibby_> leaverou: would be equivalent to the token sequence
<dlibby_> emilio: but what sequence? font-stretch as an example. you need to get interoperable serialization for all properties
<dlibby_> leaverou: don't you need that anyways?
<dlibby_> emilio: yes, just pointing out it is a lot of work
<dlibby_> Rossen_: this is indiciative of how long it might take to implement
<dlibby_> emilio: seems fine to implement
<castastrophe> q+
<fantasai> [what emilio is saying is that as a prerequisite to this, we need to get interop on serialization of all properties, which is something we don't have yet]
<fantasai> [that requires a bunch of spec work, QA work, and implementer follow-up to fix all the inconsistencies]
<dlibby_> castastrophe: for container queries, computing ancestors comes down to a 'contract' as things are flagged as cascading 'down'
<leaverou_> q?
<TabAtkins> q+ TabAtkins
<dlibby_> castastrophe: can we use a similar mechanism (!inherited) to indicate such a pattern
<Rossen_> ack castastrophe
<TabAtkins> ack TabAtkins
<dlibby_> emilio: i don't think that is necessary, as long as you inherit with the same property, you already need the complete ancestor style
<smfr> q+
<dlibby_> TabAtkins: shouldn't need that since there is no circularity
<Rossen_> ack smfr
<leaverou_> q?
<dlibby_> smfr: concerned about impl complexity vs. usefulness. in the example, you could say inherit z-index to a font property
<dlibby_> leaverou: there are use cases that you have to use a mixture of properties
<dlibby_> emilio: this would be implemented via existing serialization rules, so this doesn't explode, you don't convert between properties, use serialization syntax, then reparse token stream
<dlibby_> leaverou: in most cases you'll probably end up with an invalid declaration
<dlibby_> Rossen_: hearing consensus on what we want to achieve, but some warning on how long this might take
<dlibby_> Rossen_: any other points on the topic?
<dlibby_> leaverou: proposed resolution - add an inherit function to values-4 to get the value of parent properties
<dlibby_> RESOLVED: adopt an 'inherit' function to values-5
andruud commented 3 years ago

Am I right to assume that this is also equally feasible as long as it's a specific ancestor (e.g. 3 levels up) and not a search

@LeaVerou We should not do that. A specific ancestor is not much better than a search. It would mean that a change in any property on an element (also non-inherited ones) has the potential to invalidate the styles of all elements in that subtree. This is much worse than explicit inheritance (which only directly affects the immediate children), and I only like this feature if it's not significantly worse than explicit inheritance. :-)

Can you share a 3-levels-up use-case?

LeaVerou commented 3 years ago

@andruud

Ok, if that's not possible and it can only look at immediate parent, that's still useful. But to answer your question, these seem to be the main categories of these use cases, ordered by descending prevalence:

A lot of this can be worked around if we define parent() to expand to the equivalent token sequence at the point of use instead of being inherited as a parent() call. So, if I want to reference a grandparent value, I can just assign it to a custom property on the parent. Would this create the same issue?

Alternatively, would being able to target specific ancestors be workable? (e.g. shadow root, grandparent, root)

andruud commented 3 years ago

define parent() to expand to the equivalent token sequence at the point of use

The parent() function needs to resolve at the same time as var(), which means computed-value time. So yes, you could then assign to a custom prop to propagate the grandparent value. :-)

getify commented 3 years ago

The only reason I want to use parent(..) is to avoid the hack of assigning to a custom prop just so its value can be inherited/accessed by a child. I've already been doing that a lot for a few years. It works, but its definitely more boilerplate'y.

So if the parent(..) pattern in practice still requires that as soon as you need to go more than one level up, I'd say it's probably not going to help my use-cases nearly as much. If I have to use custom-props for this inheritance access in some or many of my uses, I'll probably stick to that same technique in all of them, for consistency.

parent(..) would in effect just be a special case of the custom-props technique, rather than a clearly more declarative alternative. Using it alongside custom props doing "the same thing" but up further than one level, that seems like it would make code less clear rather than better.

I'd urge some non-custom-prop way of "chaining" access... perhaps something like this:

.foo {
   width: 100px;
}

.foo > .bar {
   width: calc( parent(width) * 0.5 );
}

.foo > .bar > .baz {
   width: calc( parent(parent(width)) * 0.2 );
}

I also see it as a bit of a maintenance footgun that parent(..) is so restricted by DOM structure. CSS lets me write selectors with a combinator like " " (general descendant) so that I can match a "child" even if I later wrap another DOM element (like a span) in between.

But any such DOM changes would totally break any of these absolute parent(..) references.

Maybe parent(..) could be more flexible, with an optional "selector", like this:

width: parent(width);   /* default to just the direct DOM parent */

height: parent(height, .foo);
/* pull the `height` property from the ancestor matching the `.foo` selector */
Loirooriol commented 3 years ago

The only reason I want to use parent(..) is to avoid the hack of assigning to a custom prop

This is currently a hack because you need something like

.foo { /* the actual value has to be set to a custom prop :( */
  --width: 100px;
  width: var(--width);
}
.foo > .bar {
   width: calc( var(--width) * 0.5 );
}
.foo > .bar > .baz {
   width: calc( var(--width) * 0.2 );
}

With parent(), I guess you could just

* {
  --parent-width: parent(width);
}
.foo { /* no hack :) */
  width: 100px;
}
.foo > .bar {
   width: calc( parent(width) * 0.5 );
}
.foo > .bar > .baz {
   width: calc( parent(--parent-width) * 0.2 );
}
castastrophe commented 3 years ago

I mentioned this briefly in our F2F call, but I keep mulling it over in my mind so I wanted to put it in writing. One way to work around the ancestor hurdle is to create a contract by flagging a specific property as "inheritable". This could create a token (or something that functions similarly to a custom property) that cascades and is accessed by an inherited function. Taking the previous example and converting it could look something like:

.foo {
  width: 100px !inheritable;
}
.foo > .bar {
   width: calc( inherited(width) * 0.5 );
}
.foo > .bar > .baz {
   width: calc( inherited(width) * 0.2 );
}
andruud commented 3 years ago

@Loirooriol Eeek! *-selectors! :slightly_smiling_face: (Clever, though).

@castastrophe Doesn't seem like a huge value-add vs just using a custom property. Am I the only one who thinks that's not a "hack", and actually a good way to do it? :stuck_out_tongue:

If "deeply" inheriting stuff is the primary motivation for this feature, then perhaps we should just drop it, since we already have a way to do that.

castastrophe commented 3 years ago

@andruud Doesn't the computed value come into play though? If you assign 5em to a custom property, it's being evaluated at the time it's being called, correct? What if you want the value of 5em at a particular spot in the stack?

* {
  font-size: 16px;
}
.foo {
  font-size: 2em !inheritable; // computed value stored from .foo in the token
}
.foo > .bar {
   font-size: 12px;
}
.foo > .bar > .baz {
   width: calc( inherited(font-size) * 20 ); // value of 2em at .foo rather than .baz
}

Random really basic codepen for anyone reading the thread who is not familiar with how and when custom properties resolve in the cascade: https://codepen.io/castastrophee/pen/eYgVwVR

andruud commented 3 years ago

That's true. Of course you also can achieve that (in this particular case anyway) with @property.

getify commented 3 years ago

Am I the only one who thinks that's not a "hack"

FWIW, I think the "hack" part is not the inheritance, but this in particular:

.foo {
   --width: 20px;
   width: var(--width);
}

/* as opposed to: */

.foo {
   width: 20px;
}

Having to externalize the value just so it can be inherited is the "hack", IMO. @castastrophe's suggestion is a bit more ergonomic/readable in that sense, in that you declare the inheritability rather than having to imperatively create the effect in two separate steps.

If "deeply" inheriting stuff is the primary motivation for this feature

I wouldn't characterize my use-cases with "primary", but I would say "substantial" as opposed to "trivial" or "limited".

I also wouldn't say "deep", per se, as there is a practical limit to how far up the tree I might need to go... for my purposes, I'd guess that limit is somewhere around 3 or 4 levels of nesting, not 10 or 100.

That's why parent(parent(width)) is, while a bit inelegant, somewhat reasonable, because the number of wrappings I might need to do would rarely if ever be >= 4. But such a syntax would become completely untenable at 100, 10, or probably even 5. At worst, that ugliness would serve as a strong hint (code smell) of probably needing to restructure things.

That said, the maintainability footgun around such absolute traversal steps still concerns me quite a bit.

LeaVerou commented 3 years ago

I mentioned this briefly in our F2F call, but I keep mulling it over in my mind so I wanted to put it in writing. One way to work around the ancestor hurdle is to create a contract by flagging a specific property as "inheritable". This could create a token (or something that functions similarly to a custom property) that cascades and is accessed by an inherited function. Taking the previous example and converting it could look something like:

.foo {
  width: 100px !inheritable;
}
.foo > .bar {
   width: calc( inherited(width) * 0.5 );
}
.foo > .bar > .baz {
   width: calc( inherited(width) * 0.2 );
}

If you have "access" to the declaration to set !inheritable, you can also just set a custom property and have it inherit and use that on the property as well. The way I see it, this is very useful for libraries that need to work with host code that they don't control.

castastrophe commented 3 years ago

If you have "access" to the declaration to set !inheritable, you can also just set a custom property and have it inherit and use that on the property as well.

@LeaVerou This is true except for the point I was trying to make above about the computed value which is not an issue when setting px but if you are using context-centric units like ems, becomes a concern. I updated my comment to include a super basic codepen demonstrating this where we see the value of --size changes depending on where in the cascade the var is called. @property may address this, I wasn't sure how far along that was though in being defined.

LeaVerou commented 3 years ago

If "deeply" inheriting stuff is the primary motivation for this feature, then perhaps we should just drop it, since we already have a way to do that.

It is not. Most use cases are perfectly fine with parent.

@castastrophe @property is shipped in Blink, so pretty far along.

LeaVerou commented 3 years ago

That's why parent(parent(width)) is, while a bit inelegant, somewhat reasonable, because the number of wrappings I might need to do would rarely if ever be >= 4. But such a syntax would become completely untenable at 100, 10, or probably even 5. At worst, that ugliness would serve as a strong hint (code smell) of probably needing to restructure things.

Do note that accessing ancestors, if it were to be added, it would be done through some sort of parameter on parent(). Nesting calls to parent() like this wouldn't work, since the innermost parent() would return a token stream, not a property name, which is what parent() expects as an argument.

castastrophe commented 3 years ago

Here's a use-case I'm thinking of; can you help me with how parent() may or may not be useful here? It draws off of the color-contrast example above.

I have a web component (pfe-layout) with styles:

:host {
    background-color: #eee;
}

Containing a nested component (pfe-type) responsible for typography with styles:

:host {
  color: color-contrast( inherit(background-color), white, gray, black );
}

Now I could use a custom property to inherit the background color from the parent and this use-case works great, but what if an external stylesheet jumps in and sets:

pfe-layout {
    background-color: #252527;
}

The external stylesheet would have to be aware of the custom property's existence in order to correctly update the stack. We can do this with documentation, etc. A nicer experience for all involved is some form of inheritance support where pfe-type can look up it's most recent background-color context. Maybe that's not relevant to this specific discussion. I share the use-case in case it might be.

tabatkins commented 3 years ago

If I'm understanding your example correctly, it seems like that works the way you'd want, right?

That is, I'm assuming a markup structure like:

<pfe-layout>
  <::shadow>
    <style>
    :host { background-color: #eee; }
    </style>
    <pfe-type>
      <::shadow>
        <style>
        :host {
          color: color-contrast( inherit(background-color), white, gray, black );
        }
        </style>
      </::shadow>
    </pfe-type>
  </::shadow>
</pfe-layout>

So by default, pfe-layout has a light gray background, and pfe-type sets its text color to contrast against that (choosing black).

Then if the outer page sets pfe-layout { background-color: #252527; }, giving pfe-layout a dark gray background, pfe-type will still set its text color to contrast against that (choosing white).

Is there a detail I'm missing that makes this not work?


Edited before posting!

Ah, I think I see, you're saying that if pfe-type is not a direct child of pfe-layout, then it couldn't use the code you gave to directly grab 'background-color', and instead pfe-layout would have to set a custom property which then inherits down to pfe-type. And pfe-layout would have to document that users should set the background color with --pfe-layout-bg or something, right?

This shouldn't be necessary with @andruud's technique. If you have a wrapper element inside of pfe-layout's shadow, then you can set --bg-color: parent(background-color); to grab the background-color from pfe-layout, and then all descendants can just refer to that property. Since parent() is evaluated at the same time as var(), this will correctly grab the color from pfe-layout (rather than inheriting, unevaluated, to descendants, and then grabbing some random parent's value when it's actually used).

castastrophe commented 3 years ago

Ah, I think I see, you're saying that if pfe-type is not a direct child of pfe-layout

Spot on!

If you have a wrapper element inside of pfe-layout's shadow, then you can set --bg-color: parent(background-color); to grab the background-color from pfe-layout

Clever! Yes that would do it. 🥳

LeaVerou commented 3 years ago

Just came across another use case: Ability to swap variables in descendant scope.

E.g.

:root {
    --primary-color: hsl(250 30% 40%);
    --secondary-color: hsl(190 40% 40%);
}

article.alt {
    --primary-color: parent(--secondary-color);
    --secondary-color: parent(--primary-color);
}
alolalo commented 2 years ago

Hi Lea,

I wanted to put my thoughts on "inherit" here since I really like the spec, and I would love to see if it helps some of you figure this one out. I'm new to WD's but I'm not new to naming things and CSS, and I hope I can be of "perceptual assistance".

Q: what is a parent? I currently inclined to ask what does parent relate to when you say 'parent(--secondary-color)': does it mean that secondary is now set as the parent, or that --primary-color now has a parent assigned to it?

I believe that if you're going to make such a semantical and salient word intuitive to use, there must be a way to remember that pattern quite simple. I can "see" how parent works, but then again it's quite similar to inherit the way I see it; it's a sister-set that you can also do with custom properties, and what @getify was saying;

Here's my use-case:

I regularly will set a CSS variable in a parent element, then use that value for some dimension (like width). Then, in a child element, I'll use that same CSS variable and perform a calc(..) on it to derive a new value for the child element to use for some other dimension (like padding or border width). In this case, I'm only using the CSS variable because of its ability to be inherited down to the child and re-used.

It would be much nicer if, in those cases, I could skip the CSS variable altogether, and just have the child element reference a parent CSS property value, like for example: border-width: calc(parent(width) / 10).

Since this to me is more about a functional thing of layering, NOT naming imo, I would superficially divide them into four layers, like the cascade, and make each layer have one form, function & fiction: PICA

Parent(color, width, etc..) Chain something at a global level Inherit(color, width, etc..) Reference something at a local level Current(color, width, etc..) Chain something at the :root level Alias(color, width, etc..) Reference, Instantiate & Chain something at a any level without touching other references

I think that the current inherit() function is the kind of grouping that can get quite hairy perceptually, since what used to be done by yourself using css-vars, can then be done with native tools, but they don't explain cause and correllation. I think it would be far more accessible if you knew exactly which one did what, and why you need to use it where, before you need to leave your editor and go fish the docs.

In that respect, current* would work for me to substitute inherit() entirely;

currentBackground currentWidth currentParent currentColor currentBorderColor currentAccentColor

That's something that I can use out of the box without the need for a manual with a (wait for it...) parenting tone 😉

alolalo commented 2 years ago

I found this on css tricks about SVG and using two colors un and I think it's quite similar to the parent/inheritance relationship, and uses currentColor as a nice little way to avoid the shadow DOM boundary; https://css-tricks.com/lodge/svg/21-get-two-colors-use/

kizu commented 2 years ago

Was talking with @LeaVerou at CSSDay about this, just want to drop in that this feature would be really, really useful for a lot of developers, both those who already use a lot of custom properties (like me — having the need for this very often) to do even more things with them, and for those who come from preprocessors like Stylus, where there are things like property lookups (https://stylus-lang.com/docs/variables.html#property-lookup — in the Stylus' context it only goes up the tree of rules nesting, so what is described there would be slightly different from this proposal, but I think this proposal would cover a bunch of use-cases for the lookup, most of which Lea provided).

Would be really cool to see any progress on this, like to know if we still have any roadblocks like the mentioned “interop on serialization of all properties” — what is the current state of it?

WickyNilliams commented 2 years ago

I've hit a use-case for this that i will explain...

I have a UI composed of components, where various components might have parts which are position:sticky. These components can be composed arbitrarily, and i want each of the sticky elements to stack on top of one another, so that each sticky part is always visible. Imagine i have a tabs component whose tab buttons are sticky, and a table component whose headers are sticky. When nesting a table inside tabs, I want both the tab buttons and table headers to be visible always.

To achieve this right now, you need to do something like:

.tab {
  position: sticky;
  top: 0;
}

.table th {
  position: sticky;
  top: 40px; /* hard-coded height of tab */
}

This is brittle because you need to store the height of the tabs in the table component. Though at least it works! But, what happens if you have a set of tabs inside a set of tabs, with a table inside them? Or what if you want a table not inside tabs? You'll end up with a broken result in these, and many other, cases.

What would be ideal is if i could keep a running tally of how much top space has been taken up by sticky elements. So each component adds to the running total, which gets inherited by descendants, who add to it etc etc.

I think with this proposal it could be achieved like this?

:root {
  --sticky-tally: 0;
}

.tab {
  --tab-height: 80px;
  --sticky-tally: calc(parent(--sticky-tally) + var(--tab-height));

  height: 80px;
  position: sticky;
  top: calc(var(--sticky-tally) - var(--tab-height));
}

.table {
  --th-height: 40px;
  --sticky-tally: calc(parent(--sticky-tally) + var(--th-height));
}

.table th {
  height: var(--th-height); 
  position: sticky;
  top: calc(var(--sticky-tally) - var(--th-height));
}

This should support arbitrary nesting with all sticky elements stacking as intended, without resorting to JS or trying to enumerate every combination of component. Also it's nice because now table doesn't need to know about tab height, each component sets their own height and just adds to the total/tally.

WickyNilliams commented 2 years ago

OK there is actually a workaround for my use case above. You can get around the self-reference problem with two elements:

.outer { --_a: var(--a, 0) }
.inner { --a: calc(var(--_a) + 1) }

In my use-case i'm building web components, so I don't really need two elements: I can instead target the :host and it's direct descendants (either a <slot> or a wrapper element):

:host { --_a: var(--a, 0) }
:host > * { --a: calc(var(--_a) + 1) }

Here's an example codepen demoing the basic approach. And here's a more complex example with multiple components of different sizes.

LeaVerou commented 2 years ago

Btw here is what contorted things authors are doing because this is not yet available: https://dev.to/janeori/css-var-inherit-var-2-yes-you-can-without-js-2dic

kizu commented 1 year ago

Just wanted to drop-in the case that I remembered that I had where I'd really want to have inherit().

Let's say we have a typographic component that applies margin to its children via an owl selector (I know that this potentially could be done with gap, but there are uses for this outside of the gap as well, like when we want to adjust the margin between specific items etc.):

.has-gap > * + * {
    margin-top: 1em;
}

If we don't have a variable, then things are always ok. But if we'd use a variable:

.has-gap {
    --gap: 1em;
}
.has-gap > * + * {
    margin-top: var(--gap);
}

Then things would work ok unless we'd nest the same component inside itself, and would try to override the gap on it for its children:

<div class="has-gap">
    …
   <div class="has-gap" style="--gap: 0.5em">…</div>
    …
</div>

Instead of just setting the gap for the children of the inner component this would also override the margin on the component itself. With inherit() and an extra CSS variable we could do something like this:

.has-gap {
    --gap: 1em;
}
.has-gap > * + * {
    --self-gap: inherit(--gap);
    margin-top: var(--self-gap);
}

This would make it so overriding the --gap on the child won't affect what the parent tries to apply — but also provides a way for the child to explicitly override this value by setting the --self-gap.

I remember I wanted to apply this pattern for some other places — basically, anywhere I want for a component to apply some value that we get from a potentially overridable CSS variable to its children, I'd always want to use inherit().

LeaVerou commented 1 year ago

Oh that is actually a much broader use case. I've often wanted to do things like margin-top: calc(-1 * inherit(padding-top)); to have children that circumvent their parent's padding.

brandonmcconnell commented 1 year ago

One more use case to add to the pile here— I was looking for a way to increment an angular value as I traversed down the layers of the DOM for simple—dare-I-say archaic—debugging.

My process in trying to set this up was…

  1. (codepen) tried to use variable that inherit the same variable value from their parent and increment it — dead end
  2. (codepen) tried to do the same but with @property to enable inheriting the value — dead end there too, presumably because the variable does not execute the value in real-time but rather passes its reference (I'm totally guessing here)
  3. (codepen) tried to do the same but utilize counters to increment the values and then variables to inherit the calc values and then calc them — also a dead end (re https://github.com/w3c/csswg-drafts/issues/1026)

inherit() would appear to fix this issue entirely.

Here's the source for my second example listed above ☝🏼

<div class="css-debug"><div><div><div><div><div><div><div><div><div><div></div></div></div></div></div></div></div></div></div></div></div>
@property --css-debug-hue {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}

html, body {
  width: 100vw;
  height: 100vh;
  box-sizing: border-box;
  display: flex;
}

div {
  width: 100%;
  height: 100%;
  padding: 20px;
  box-sizing: border-box;
}

.css-debug, .css-debug *, .css-debug *::before, .css-debug *::after {
  --css-debug-hue: calc(var(--css-debug-hue) + 20deg);
  box-shadow: inset 0 0 0 1px hsl(var(--css-debug-hue) 100% 50%);
}

screenshot

https://codepen.io/brandonmcconnell/pen/XWYGJKy/097c4bf42871a86d5005e58fdf2183c3?editors=0110

SYwaves commented 1 year ago

If my understanding is correct, this would allow for alternating color nested divs, or even rainbow nest with significantly less syntax.

#parent-container {
--hue-variance: 20;
}

div {
--hue-variance:  calc(inherit(--hue-variance) * -1);
background-color: hsl(calc(100 + var(--hue-variance)) 30% 60%);
}
<div id="parent-container">
  <div>
  <!--hue 80-->
    <div>
    <!--hue 120-->
      <div>
      <!--hue 80-->
      </div>
    </div>
  </div>
</div>
LeaVerou commented 1 year ago

I wonder if we could take a similar approach to this as with style container queries: Add a version that only supports custom properties in css-values-5, then the full version later, and that could possibly also be gradual (e.g. longhands first, or specific properties first). Agenda+ to discuss this idea and hear from implementors.