Skip to content

Instantly share code, notes, and snippets.

@bkardell
Last active May 28, 2021 17:00
Show Gist options
  • Save bkardell/e5d702b15c7bcf2de2d60b80b916e53c to your computer and use it in GitHub Desktop.
Save bkardell/e5d702b15c7bcf2de2d60b80b916e53c to your computer and use it in GitHub Desktop.

The potential/value of a switch() function in CSS

Background

A lot of developers desire things which are currently problematic for CSS and many of these would appear to be somewhat hard to explain why. Two common kinds of problems are circularity and performance. The magic that CSS is able to do is both a blessing a a curse here. From a developer's perspective, CSS seems to resolve things that are in conflict with one another all the time or which would 'appear' to be the same kind of problem. However, it accomplishes this through carefully designing based on constraints of precisely what can happen, and when. A good example of the kinds of asks that have these challenges are those described commonly as 'container queries' problems.

The idea here is force us to focus on 'where things fit' within the architecture of CSS pretty naturally and how we might build a new pattern within CSS implementations to solve the sort of variable answers required. It allows the development of internal pathways toward solving certain classes of such problems (a new opportunity for variable answers/expression) and does so in a way that this progress can be exposed to developers in native and performant ways, in comparatively short order. They might pair this additional tools, like preprocessors or other CSS properties to help us explore the larger space. It is a step geared toward solving this initial problem - additional sugars and higher level integrations then have an underlying plausible path laid.

In several ways, the existing toggle() proposal is related and its use cases could even potentially be solved with this patten as well, so tt seems generally potentially useful and interesting regardless of whether this is the route toward ultimately solving the entire container queries problem or not:

Basic details..

The switch function allows elements make values dependent on a list of possible conditional values with relevant context instead of inheriting the same value. When processing the value of switch(), properties may provide relevant contextual values which are substituted at the appropriate time.

/* example 1 /
.foo {
	display: grid;
	grid-template-columns: switch(
	 	(available-inline-size > 1024px) 1fr 4fr 1fr;
	 	(available-inline-size > 400px) 2fr 1fr;
		(available-inline-size > 100px) 1fr;
		default 1fr;
	 );
}

The syntax of the switch() function is:

switch( <switch-value>)

where <switch-value> is a vector of <switch-condition> <css-value> pairs separated by ;`..

(Something like (TODO write this up it's handwavey/incorrect/incomplete) (<IDENT> ?(<|> <length>)) <CSSVal>; ) They are evaluated in order until one is true, and its value is used.

The switch() notation is not allowed to be nested;it may not contain attr() or calc() notations. Declarations containing such constructs are invalid. Properties may specify relevant context variables that make sense within their context. inline-available-size, for example can be made available to various properties that are computed at layout time. Expressions containing context-variables not provided by the property are invalid.

/* Example 2: invalid examples /    
  
/*   
  This example would be invalid because
  the display property would not provide 
  an available-inline-size context as 
  it cannot be known at this time.
*/
.bar {  
	display: switch(
              (available-inline-size>1024px) block;
              default: inline;
             );  
}
    
/* Invalid because this contains calc() or attr() /
.bat {
	grid-template-columns:  switch(
	 	(available-inline-size > 1024px) calc(100vh/2);
	 	(available-inline-size > 400px) 3fr;
		(available-inline-size > 100px) 2fr;
		default 1fr;
	 );
}                

As suggested earlier, the intent is that properties could definte potential values for these and there is spirtitual similarity to toggle here... For example, that might be written as something like

em { 
  font-style: switch( 
    (parent-font-style == em), normal;  
     default: italic; 
}

Similarly, while you could do this mostly with available-inline-size, it might be interesting in the future to be able to express something like 'if I am wrapped' for something like text-alignment:

.block {
   text-alignment: switch(
                     (is-wrapping) center;
                     default left;
                   );
}

Notes:

* See also David Baron's ideas here https://github.com/dbaron/container-queries-implementability for addressing some of the same use cases.

  • There is overlap - but they are mutually interesting and overlap is not complete.
  • Both contain low level next steps and these steps are both potentially valuable regardless of which way we go
  • We favor some time exploring both as possibilities - there is certainly useful information and potential in both of them

* The specific proposal is to start with inline-available-size (could bikeshed) being made available by some properties to explore this, but to lay down a pattern that could be more widely used for things that are this class of problem. Igalia is exploring this space with grid-template-columns as a small investment to further inform, no specific timeline.

* The constraints need to be clarified here, I am basing these mainly on Tab's work on toggle(). It may also be necessary for it to be the sole value, but Oriol thinks perhaps not so I'm leaving this one out for now.

@argyleink
Copy link

determined-inline-size? it's not necessarily what's available right? it's what's calculated?

@argyleink
Copy link

/* Invalid because this contains calc() or attr() /

but custom properties are cool? eg var(--foo)

@bfgeek
Copy link

bfgeek commented Apr 29, 2020

For resolving these values during the intrinsic sizes phase there are a few different options:

  1. Rely on 1D size containment.
  2. Resolve all the values with the "default" case, this is similar to how percentages are treated as "auto" for the purposes of intrinsic sizing.
  3. Resolve the switch statement twice, once using an available-inline-size of zero, and once using Infinity.

Using the previous example:

  .class {
	grid-template-columns: switch(
	 	(available-inline-size > 1024px) 1fr 4fr 1fr;
	 	(available-inline-size > 400px) 2fr 1fr;
		(available-inline-size > 100px) 1fr;
		default 1fr;
	 );
  }

Case "2" would be grid-template-columns: 1fr during the intrinsic sizes phase.

Case "3" would be grid-template-columns: 1fr for determining the min-size, and grid-template-columns: 1fr 4fr 1fr for determining the max-size.

@una
Copy link

una commented Apr 29, 2020

but custom properties are cool? eg var(--foo)

Would we be able to set this on a custom prop?

--foo: switch(
  (available-inline-size > 1024px) 1fr 4fr 1fr;
  (available-inline-size > 400px) 2fr 1fr;
  (available-inline-size > 100px) 1fr;
  default 1fr;
);

@fantasai
Copy link

fantasai commented May 7, 2020

Random syntax idea:

	grid-template-columns: switch(
	 	(available-inline-size > 1024px) 1fr 4fr 1fr;
	 	(available-inline-size > 400px) 2fr 1fr;
		(available-inline-size > 100px) 1fr;
		default 1fr;
	 );

vs

	grid-template-columns: switch( available-inline-size ?
	 	(? > 1024px) 1fr 4fr 1fr;
	 	(? > 400px) 2fr 1fr;
		(? > 100px) 1fr;
		1fr;
	 );

just to avoid retyping the variable name over and over again... If checking for equals, then just write (1024px) instead of (? = 1024).

@c-smile
Copy link

c-smile commented May 16, 2020

Shouldn't switch() be just calc() extended by cond ? true-part : false-part

So:

grid-template-columns: calc( available-inline-size > 1024px ? 1fr 4fr 1fr
                           : available-inline-size > 400px ? 2fr 1fr
                           : 1fr );

and so

width: calc( available-inline-size > 1024px ? 100px
           : available-inline-size > 400px ? 50px
           : 25px );

Yet, instead of flat names like available-inline-size I'd suggest to move them to separate namespace:

layout(inline-size)

so we can use 'variables' from different domains and environments:

width : calc( 100% / grid(total-columns) ); 

@jonathantneal
Copy link

jonathantneal commented Oct 21, 2020

it may not contain attr() or calc() notations.

Should this say “it may not contain mathematical expressions?”? Otherwise, what about min(), max(), or clamp()?


As other have asked, what if I put switch() on a custom property and then use that custom property in some other declaration within a mathematical expression? Is it, like, tainted?

available-inline-size
parent-font-style


If possible, could a consistent term apply to these technologies? One alternative could be available-inline-size and available-font-style. Regardless of what the actual words are, if they are consistent it would seem helpful for inferring values when there are more in the future.


Related, I agree with the earlier suggestion to terse the syntax. Perhaps functional terms could also contain the condition.

.foo {
  display: grid;
  grid-template-columns: switch(
    available(inline-size > 1024px): 1fr 4fr 1fr;
    available(inline-size >  400px): 2fr 1fr;
    available(inline-size >  100px): 1fr;
    default 1fr
  );
);
.foo {
  display: grid;
  grid-template-columns: switch(
    available(inline-size > 1024px):
      1fr 4fr 1fr;
    supports(background: double-rainbow()):
      2fr 1fr;
    media(pointer: fine) and available(font-style: italic):
      1fr;
    default 1fr
  );
);

@bkardell
Copy link
Author

but custom properties are cool? eg var(--foo)

Would we be able to set this on a custom prop?

--foo: switch(
  (available-inline-size > 1024px) 1fr 4fr 1fr;
  (available-inline-size > 400px) 2fr 1fr;
  (available-inline-size > 100px) 1fr;
  default 1fr;
);

Sorry for the massive delay, I didn't see that people were commenting here. Yes @una, I think that should work (at least it works in our early prototype stuff so far)

@jonathantneal
Copy link

The syntax of the switch() function is:

Based on your existing examples it might be this:

switch() = switch( <switch-clauses>* )

<switch-clauses> = <switch-clause> <switch-statement>
<switch-clause> = <switch-expression> | <switch-default>
<switch-expression> = ( <ident> <se-comparison> <length> )
<switch-default> = 'default'
<switch-statement> = <declaration> [ ';' <declaration> ]* ';'?

<se-comparison> = <se-lt> | <se-gt> | <se-eq>
<se-lt> = '<' '='?
<se-gt> = '>' '='?
<se-eq> = '=' | ':'

@adactio
Copy link

adactio commented Nov 14, 2020

The switch() syntax makes sense to me.

One bit of feedback I got from one of my colleagues is that they'd really like to be able to use custom properties in the switch conditions:

grid-template-columns:  switch(
	 	(available-inline-size > var(--myValue) ) 1fr 4fr 1fr;

This isn't something that's possible with media query conditions, which is a real shame. If we could avoid repeating that with container queries, it would be wonderful (though I have absolutely no idea who difficult it is from an implementor perspective to allow custom properties inside conditions).

@brandonmcconnell
Copy link

it may not contain attr() or calc() notations

The switch() syntax makes sense to me too. I'm curious why it wouldn't or shouldn't support nesting such notations/methods as attr() and calc(), or being nested in them. It seems as though it would be a great enhancement to support this.

One such example I can see would be, piggy-backing on @jonathantneal's comment…

.foo {
  display: grid;
  font-size: switch(
    media(min-width: 399px): calc(5vw - 10px);
    media(min-width: 799px):  24px;
    default clamp(20px, 10vw, 56px);
  );
);

Would this simply be withheld from the switch() statement's initial implementation, or is the intention for switch() to never play nicely with these other mathematical functions. It would certainly be quicker for the developer, and likely the CPU, to write/process these different computations, first filtered through a switch() rather than computing all values at all times, as would be the case in traditional CSS.

I'm coming at this with a fresh take, so shoot me down if my comments here are off-base. Simply looking to help collaborate on the future of CSS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment