w3c / csswg-drafts

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

map-get function proposal for customising web component styles #7273

Open muratcorlu opened 2 years ago

muratcorlu commented 2 years ago

I want to start a discussion about having a map-get function in CSS like in older versions of SASS.

Need:

When we use HTML Custom Elements (a.k.a Web Components), Shadow DOM is a powerful opportunity to isolate components style, and using CSS Custom Properties (a.k.a. CSS Variables) for customizing components styles is a very convenient approach. But in some scenarios, we are very limited in providing some good limited options to our component consumers as a component developer.

For example;

I want to provide a button component that can be in a limited set of sizes regarding our design system. So we have variants like large, regular or small. Currently, many people use component attributes like <my-button small> but this is not an ideal way when you think about a responsive design. Instead, I would prefer to be able to say:

my-button {
  --size: small;
}

To be able to have that kind of clean solution we need to have a solution to map this small naming to a different type of units inside component styles.

Proposal

As in old map-get function of SASS, having an opportunity to set a value by picking from a map would be great like:

/* Inside our component style */
button {
    padding: map-get((small: 4px  8px, regular: 8px 16px, large: 12px 24px), var(--size, regular));
}

Means: regarding to value of --size variable (regular by default) set padding to 4px 8px if value is small.

So a consumer can not set the padding of my button component other than the options that I set here. Also a consumer can customize this property with a responsive design approach:

my-button {
   --size: regular;
}

@media screen and (max-width: 900px) {
  my-button {
    --size: large;
  }
}

This can be also used as conditional values for non size units:

/* Inside our component style */
button {
    background-color: map-get((primary: #336699, secondary: #006600), var(--variant, primary));
}

Means: regarding to value of --variant variable (primary by default) set background-color to #006600 if value is secondary.

So, the structure is map-get( (mapObject), key ) I'm not sure what is the best format for defining a map in CSS with current technical background, but JSON-like key: value, structure would be very familiar for many front-end developers.

I hope I could express the motivation very well. Any ideas and questions to clarify details are very welcome.

johannesodland commented 2 years ago

Love the idea to control component styles based on a custom property.

There's already a discussion on higher level custom properties that might solve the use case.

Container queries could also solve part of the use case: https://github.com/w3c/csswg-drafts/issues/5624#issuecomment-849091911 https://drafts.csswg.org/css-contain-3/#container-rule

my-button {
  container: my-button / style;
}

@container my-button style(--size = regular) {
  /* styles */
}
muratcorlu commented 2 years ago

@johannesodland Thanks for mentioning other discussion. I couldn't notice it before. I also mentioned my proposal there. For me, other discussion proves a need to set custom variables for web components in a better way. Hopefully, we can have a consensus on a solution.

muratcorlu commented 2 years ago

Another more CSS-familiar syntax would be:

padding: map-get(small 4px 8px, regular 8px 16px, large 12px 24px, var(--size, regular));

Structure above is map-get( mapItem, mapItem, ... , key) and mapItem is key value (value is everything until comma). Then maybe this can also allow us using CSS variables for map items too.

:host {
  --padding-sizes: small 4px 8px, regular 8px 16px, large 12px 24px;
  padding: map-get(var(--padding-sizes), var(--size, regular));
}
Loirooriol commented 2 years ago

This is like the nth-value() function discussed in https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-626072319

But instead of

my-button {
  --size: small;
}
button {
  padding: map-get((small: 4px  8px, regular: 8px 16px, large: 12px 24px), var(--size, regular));
}

you would use natural indices:

:root {
  --small: 1;
  --regular: 2;
  --large: 3;
}
my-button {
  --size: var(--small);
}
button {
  padding: nth-value(
    var(--size, var(--regular));
    /*small*/   4px   8px;
    /*regular*/ 8px  16px;
    /*large*/   12px 24px
  );
}
muratcorlu commented 2 years ago

Interesting approach. That also could solve the problem. The only big problem I see here is that this will require defining many enum-like values (--small: 1) outside of the web component styles.

LeaVerou commented 1 year ago

@Loirooriol

This is like the nth-value() function discussed in #5009 (comment) [snip]

Once we have if(), we don't need to change the user-facing API to accommodate CSS' limitations, we can just do:

my-button {
    --size: small;
}

button {
    --size-index: if(--size: small, 1, if(--size: regular, 2, if(--size: large, 3)));
    padding: nth-value(--size-index, ...);
}

Though some sugar to make this less painful might be nice.

muratcorlu commented 1 year ago

Nested ifs looks a bit difficult to read and too crowded since you repeat if(--size: {someting} every time.

In case of using for an index;

--size-index: map-get(small 1, regular 2, large 3, var(--size, regular));

vs

--size-index: if(--size: small, 1, if(--size: large, 3, 2));

I still find mapping values more readable than if conditions.

Even more readable:

--size-map: small 1, regular 2, large 3;
--size-index: map-get(var(--size-map), var(--size, regular));

Once we have map-get function we don't need to work with indexes though:

:host {
  --padding-sizes: small 4px 8px, regular 8px 16px, large 12px 24px;
  padding: map-get(var(--padding-sizes), var(--size, regular));
}
LeaVerou commented 1 year ago

Actually, I just remembered that I had defined if() with the second parameter optional, and it defaults to an empty value. This means you can do it like this:

my-button {
    --size: small;
}

button {
    --size-index: if(--size: small, 1) if(--size: regular, 2) if(--size: large, 3);
    padding: nth-value(--size-index, ...);
}

(I should also change the syntax to use ; for a separator, so the values can contain commas)

Crissov commented 1 year ago

you would use natural indices:

:root {
  --small: 1;
  --regular: 2;
  --large: 3;
}
my-button {
  --size: var(--small);
}
button {
  padding: nth-value(
    var(--size, var(--regular));
    /*small*/    4px  8px;
    /*regular*/  8px 16px;
    /*large*/   12px 24px
  );
}

With predefined readable indices, this could look something like this:

my-button {
  --size: small;
}
button {
  padding: choose(
    var(--size, medium);
    small:   4px  8px;
    medium:  8px 16px;
    large:  12px 24px
  );
}

or even like this:

my-button {
  --size: small;
}
button {
  padding: choose(
    var(--size, medium);
     4px  8px;
     8px 16px;
    12px 24px
  );
}

Example predefined indices

Numerical index Size Shirt Screen SI Metric Lightness
-2 tiny XS watch micro centi darkest
-1 small S, SM mobile, phone milli deci dark
0 medium M, MD tablet unity unity moderate
1 large L, LG laptop kilo deca, deka light
2 huge XL desktop mega hecto lightest
@index-style size {
  -2: XS, extra-small, tiny;
  -1: S, SM, small;
   0: M, MD, medium;
   1: L, LG, large;
   2: XL, extra-large, huge;
}

button {
  padding: choose(
    var(--size, medium) size;
     4px  8px; /* =< -1 */
     8px 16px; /* ==  0 */
    12px 24px; /* >= +1 */
  );

  margin: choose(
    var(--size, medium) size;
    4px; /* =<  0 */
    6px; /* >= +1 */
  );

}