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

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

Open
muratcorlu opened this issue May 12, 2022 · 9 comments
Open

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

muratcorlu opened this issue May 12, 2022 · 9 comments

Comments

@muratcorlu
Copy link

muratcorlu commented May 12, 2022

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
Copy link

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:
#5624 (comment)
https://drafts.csswg.org/css-contain-3/#container-rule

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

@container my-button style(--size = regular) {
  /* styles */
}

@muratcorlu
Copy link
Author

@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
Copy link
Author

muratcorlu commented May 13, 2022

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
Copy link
Contributor

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

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
Copy link
Author

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
Copy link
Member

LeaVerou commented Feb 13, 2023

@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
Copy link
Author

muratcorlu commented Feb 15, 2023

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
Copy link
Member

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
Copy link
Contributor

Crissov commented Feb 19, 2023

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 */
  );

}

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

6 participants