Custom property selectors


#1

This is likely to cause an edge case in looping that crops up in most threads about custom properties.

However I would like to propose two extensions to the syntax to generate variables on the fly:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
}

my-flash-message {
  [use=--use] {
    my-heading {
      color: var(--use-var(--use)-lighter, --use-info-lighter);
    }
    my-block {
      color: var(--use-var(--use)-light, --use-info-light);
    }
  }
}

Where I have a custom element:

<my-flash-message use="warning">...</my-flash-message>

The suggestions are:

  • Variable variable syntax: var(--use-var(--use)-light) or var(--calc(use- + var(--use) + -light))
  • Attribute selector variables: [use=--use] where use="warning" makes var(--use) equal ‘warning’

The rationale is:

Having the ability to create a property that adds semantic meaning to custom elements that defines style dependent on the custom element and context (Top level selectors wouldn’t work). Generated macro code works for this use case somewhat however the output selectors becomes very large quickly.


#2

It looks like your idea is to enable concatenating strings together and using that as a variable name? Isn’t this just as easily achieved with something like:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
}

my-flash-message {
    my-heading {
      color: var(--use-info-lighter);
    }
    my-block {
      color: var(--use-info-light);
    }
}
my-flash-message[use=warning] {
  my-heading {
    color: var(--use-warning-lighter);
  }
  my-block {
    color: var(--use-warning-light);
  }
}

Or, if you use these variables multiple times and want to make it a bit more DRY:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
}

my-flash-message {
    --color-light: var(--use-info-light);
    --color-lighter: var(--use-info-lighter);
    my-heading {
      color: var(--color-lighter);
    }
    my-block {
      color: var(--color-light);
    }
}
my-flash-message[use=warning] {
    --color-light: var(--use-warning-light);
    --color-lighter: var(--use-warning-lighter);
}

#3

Our current site has 9 of those ‘uses’ which always use the same ratios just different ‘modifiers’ and properties for each component.

So for example we have: ‘warning’, ‘error’, ‘info’, ‘success’ that all look the same just different shades.

The practice works really well, just the CSS the LESS macros we are using generates a lot of noise.


#4

Yes that is the suggestion here.

I have been looking for a way in which the template can provide semantic meaning to components from the outside and this is the best I have come up with so far.


#5

Our current site has 9 of those ‘uses’ which always use the same ratios just different ‘modifiers’ and properties for each component.

Then it looks like my second suggestion will work fine - just cast the correct set of variables into a common set at time of use, and use that common set.


#6

Certainly it is possible but with 50 odd components each using ‘use’ in this manner it would end up with pretty much double the CSS we currently have.

It is being implemented with something similar to this in LESS:

  my-component {
    .generate-use(color, dark)
    .generate-use(background-color, lighter, my-other-component)
  }

Unless you mean more like:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
  --color-light: var(--use-info-light);
  --color-lighter: var(--use-info-lighter);
}
[use=warning] {
    --color-light: var(--use-warning-light);
    --color-lighter: var(--use-warning-lighter);
}
[use=info] {
    --color-light: var(--use-info-light);
    --color-lighter: var(--use-info-lighter);
}
my-flash-message {
    my-heading {
      color: var(--color-lighter);
    }
    my-block {
      color: var(--color-light);
    }
}

Would that work as intended?


#7

Also I only just noticed we are relying on LESS style nesting here, rather than your suggested syntax.


#8

Yes, that was my second suggestion exactly.


#9

With some mild tweaks of making the attribute selectors top level etc, I’m always paranoid about syntax usage I can’t test :smiley:

That covers my use cases however it will to my understanding break where the LESS properties don’t.

Due to assigning variables, across components the cascade will define properties for all children so that is why the defaults need to be per component don’t they? In your example:

my-flash-message {
    --color-light: var(--use-info-light);
...

Otherwise both components here have warning usage:

<my-component use="warning">
<my-component />
</my-component>

A way to ‘default’ custom properties may become useful over time for things like this, so you could:

my-component {
  --[^color]: default;
  or
 --color*: default;
}

#10

I don’t understand what you mean. In my example code, the message components will get the info colors by default, but warning messages will override them to use the warning colors (and specificity ensures that they’ll win).

It seems like maybe you think there’s some notion of ordering going on here, such that the value set at one point in the DOM applies for the rest of the page until set otherwise? Or something like that, I’m a little confused. Custom properties are just inherited properties - when you set them, they apply to the element and its descendants, nothing more.

Defaulting a custom property is indeed in the cards for Custom Properties level 2, where we’ll make them real, useful properties, rather than just trivial hosts for variable values. You’ll be able to set an “initial” value for them, etc.


#11

No not ordering just that the standard DOM rules is different to how I am using it with components, I am making the components all throw out a default ‘use’.

So I understand variables would be no different to variable usage:

So the above has to reset div to blue (which is what you did, however in my example I didn’t reset at the my-flash-message).

The LESS I showed outputs similar to:

my-component[use=info] {
  color: blue;
}
my-component[use=info] my-other-component {
  color: lightblue;
}
my-component[use=warning] {
  color: red;
}
...

This is very noisy as output however when defaulting the components to have a ‘use=“normal”’ with JavaScript I can skirt round changing all the properties per component in the CSS.

Resetting variables for each component would need to know to reset to [use=*] rather than initial to be useful I feel.

It’s a weak sounding use case however I think some kind of reset per component may close the circle on this.

So lets say this imaginary syntax:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
  --color-light: var(--use-info-light);
  --color-lighter: var(--use-info-lighter);
}

@initial {
  [use=warning] {
      --color-light: var(--use-warning-light);
      --color-lighter: var(--use-warning-lighter);
  }
  [use=info] {
      --color-light: var(--use-info-light);
      --color-lighter: var(--use-info-lighter);
  }
}
my-flash-message {
    all: initial;
    my-heading {
      color: var(--color-lighter);
    }
    my-block {
      color: var(--color-light);
    }
}

#12

This is identical to just using a normal style rule. That is, just strip off the surrounding @initial block, and you’ve got correct CSS that does what you want.

(Note as well that ‘all’ does not reset custom properties.)


#13

Yup it is however given the following structure:

<my-modal use="warning">
  <my-header>
    This is my modal
  </my-header>
  <my-content>
    text here
  </my-content>
</my-modal>

I want to be able to easily set:

my-content {
  .use-normal();
}

So the visual appearance would be the header is a background red, but the content behaves as if the cascade of [use=warning] doesn’t apply.

So I was trying to bikeshed a syntax that basically removes the cascade unless my-content had it’s own ‘use’.

As in the syntax we used --color-light would be --color-warning-light as it would cascade down right?


#14

Is that something that might be possible with variables 2, @tabatkins?

Seems like something your custom alias spec could cover perhaps or even allow @extend to support variables too might cover this use case.


#15

So I was trying to bikeshed a syntax that basically removes the cascade unless my-content had it’s own ‘use’.

I think you’re confusing the cascade with inheritance. If you set the variables to their “warning” values with a my-model[use=warning] rule, then a rule that specifies the “normal” values for my-content will work just fine.


#16

Yup I did mean inheritance sorry (terminology aside, I still mean the same).

That is right but instead of having to do something like this:

my-modal my-content {
  --color-light: var(--use-normal-light);
  --color-lighter: var(--use-normal-lighter);
}

I was thinking a way to define the DOM or CSS with the ability to reset to a defined value with the following advantages:

  • Remove the need to redefine what normal is everywhere
  • Removes the need to declare set rules (like ‘use’) everywhere
  • Adding new definitions doesn’t mean reopening up many CSS files to hunt down variable usage
  • Template authors won’t need redefine use="normal" at all levels

CSS nesting specification
#17

What I want should be possible with @extend and the % class syntax assuming this would be possible:

:root {
  --use-info-light: blue;
  --use-info-lighter: lightblue;
  --use-warning-light: red;
  --use-warning-lighter: pink;
}
%use {
  --color-light: var(--use-info-light);
  --color-lighter: var(--use-info-lighter);
}
%use[use=warning] {
  --color-light: var(--use-warning-light);
  --color-lighter: var(--use-warning-lighter);
}
%use[use=info] {
  --color-light: var(--use-info-light);
  --color-lighter: var(--use-info-lighter);
}
my-flash-message {
    @extend %use;
    my-heading {
      color: var(--color-lighter);
    }
    my-block {
      color: var(--color-light);
    }
}