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

[css-values] if() function #3455

Closed
matthew-dean opened this issue Dec 19, 2018 · 11 comments
Closed

[css-values] if() function #3455

matthew-dean opened this issue Dec 19, 2018 · 11 comments

Comments

@matthew-dean
Copy link

The addition of min(), max(), clamp() and toggle() to the CSS Values 3 draft has made a lot of great progress towards more expressive styles. One thing that would be a very powerful general purpose logic switch would be an if() function. (Full disclosure, this matches the implementation in Less - http://lesscss.org/functions/#logical-functions-if, but would be much more powerful in the cascade.)

It would have this signature:

if(condition, trueResult, falseResult)

The condition would match the semantics in Media Queries Level 4, and would query the computed properties of an element.

For example, here's usage of an if() as an emulation of min()

.foo {
  --calc: calc(10 * (1vw + 1vh) / 2);
  font-size: if(var(--calc) < 12px, 12px, var(--calc));
}

Obviously, we already have min max, but what's useful about if() is that, like toggle(), you can use it for any value.

.foo {
  /** If our inherited width is below a certain threshold, change flex box wrapping behavior */
  flex-wrap: if(100% < 400px, wrap, nowrap);
  /** Note, a nicer way to write this would be "width < 400px" or some kind of units for inherited container width / height, like "100cw < 400px" but those don't exist */
}

You could also use it to toggle background based on inherited values.

.foo {
  background: if(currentColor = white, black, lightblue);
}

Like calc() and like other languages that have a declarative if() function, you can of course nest it.

.foo {
  width: if(box-sizing = border-box, 100%, if(var(--adjust), calc(100% - 20px), 100%));
}

Note that many other functions in CSS already have a kind of logic test built-in, but they're very restricted, such as var() itself, which tests for an inherited value, and has an else if the value is not defined.

if() would also pair nicely with toggle(), where the toggled value can be tested to resolve other property values.

.foo {
  font-style: toggle(italic, normal);
}
.foo .bar {
  /** Also demonstrates use of MQ4 not() */
  text-transform: if(not (font-style = italic), uppercase, inherit);
}

Anyway, that's for your consideration. Cheers!

@tomhodgins
Copy link

tomhodgins commented Dec 19, 2018

Very interesting idea! It got me thinking if there was a way we might be able to express a condition to test, and two results to pick between returning using CSS variables, and I think I have a basic demo of something that can work, but it's limited to only what JavaScript knows about (so right now I'm not sure how it would compare CSS units).

demo: https://codepen.io/tomhodgins/pen/jXVMPW

We can invent our own CSS variables like --if-1 or if-2, or --if-computed, or whatever we want to named them and put them either in our CSS like this:

:root {
  --if-1: '{"equal": [5, 5]}, "lime", "hotpink"';
  --if-2: '{"equal": [5, 4]}, "lime", "hotpink"';
}

Or set them on a tag in HTML like this (I wish the quotes didn't have to be encoded 😇):

<html style='--if-computed: "{%22computed%22: [%22background-color%22, %22rgb(0, 255, 0)%22]}, %2220pt%22, %2210pt%22"'>

Then we can write a function that supports the various conditions we want to test — whether it's mathematical comparisons between numbers, or checking the computed styles of properties on elements — anything that JavaScript can test for us can be written as a condition. In this demo I'm supporting <, <=, ===, >=, >, and checking if the computed value of a CSS property matches a value. We can encode that in a format that's easy for us to read from CSS using JSON.parse(), and then we can use what we find to pick the right comparison function to use, to compare a left and right hand side of a comparison, and then to return a different result whether the test is true or false:

function comparison(value, event, ruleOrTag) {
  const [conditionObject, trueResult, falseResult] = JSON.parse(`[${decodeURI(value)}]`)
  const [name, [left, right]] = Object.entries(conditionObject)[0]
  const features = {
    less: (left, right) => Number(left) < Number(right),
    lessEqual: (left, right) => Number(left) <= Number(right),
    equal: (left, right) => Number(left) === Number(right),
    greaterEqual: (left, right) => Number(left) >= Number(right),
    greater: (left, right) => Number(left) > Number(right),
    computed: (left, right) => window.getComputedStyle(ruleOrTag)[left] === right
  }
  return features[name](left, right) ? trueResult : falseResult
}

And then all we need to do to connect the dots between CSS and JS is this to loop through all of the rules in CSSOM and the tags in DOM, find all CSS variables that contain the information we want to pick between, and process what we find. For this I'm using a computed-variables plugin with a configuration like this:

computedVariables('--if-', comparison, window, ['load'])

This will find every CSS variable starting with --if- and run what it finds through our comparison function, being processed when the load event fires on the window. Now anywhere we use var(--if-1) or var(--if-2) or var(--if-computed) in CSS we'l have the correct result for whatever condition we tested with JavaScript immediately available to use anywhere in CSS.

How far do you think JavaScript + CSS variables could go toward implementing this sort of if() functionality in CSS as we have it today?

@matthew-dean
Copy link
Author

@tomhodgins ? 🤔 Are you just pitching your JS-in-CSS solution? I don't see how that's related to what I proposed. Sure you can simulate this in JS by parsing CSS vars. How is that relevant? 🤷‍♂️

@tomhodgins
Copy link

Are you just pitching your JS-in-CSS solution?

I didn't invent CSS variables, I wrote a demo based on what you described that leveraged the built-in features of CSS. Everything I have done is something that can be done with CSS as it currently is specced and supported in browsers. I'm pitching using CSS.

Sure you can simulate this in JS by parsing CSS vars. How is that relevant? 🤷‍♂️

Isn't this why CSS variables exist? If this gets close to what you're hoping to have someday in CSS, I wonder to what extent you can make use of CSS variables to simulate this sort of functionality in the meantime.

@matthew-dean
Copy link
Author

matthew-dean commented Dec 19, 2018

I wonder to what extent you can make use of CSS variables to simulate this sort of functionality in the meantime.

You can do pretty much whatever you want with CSS variables, but because CSSOM is still in draft, any changes you make in actual output usually involves an entire replacement of a stylesheet's innerHTML, which will invalidate the style tree and can cause a document style recalculation / reflow. You can do it on a limited scale, but a JavaScript synchronous read / update process can never be as fast or as performant as a solution in CSS. The browser can immediately begin calculating a style tree very early in building the document. JavaScript-based solutions have to wait, look for styles, parse, calculate changes, and push updates, with more expensive side-effects. I know you've done a lot of impressive demos of what can be done with CSS variables, but it can never beat a native feature, especially because simulating "cascade effects" through JavaScript is difficult. It's why I would advocate for this even though it's in Less, because live variable bindings / cascade make it a more powerful possibility than a pre-processor (or JS live processor) can provide.

Isn't this why CSS variables exist?

If you mean to be hook points for JavaScript polyfills, no. That is not their primary design purpose. That's mainly the goal of worklets and the CSS Paint API (Houdini).

@tomhodgins
Copy link

You can do it on a limited scale, but a JavaScript synchronous read / update process can never be as fast or as performant as a solution in CSS … it can never beat a native feature

Definitely agreed, but short of actually implementing this in a browser how do can this sort of functionality be prototyped and explored? Building a demo to try it out using technology we already have seems like a good starting point.

I'm not trying to say this shouldn't be a feature, the opposite. I'm trying to explore the idea and establish:

  • the feature could be useful
  • if existing solutions to it (like what I've built) aren't adequate

That's why I'm so confused by your reaction to this. I would have thought somebody wanting this feature like this would be excited about a demo, even if it's rough, because it proves something about the idea.

If you think the idea is useful and that my demo isn't good enough to use in production, that's something good we have proven about it, and anybody can run the demo to see why it's not good enough for themselves and understand. Having the demo helps make the case for the feature. And having more demos should help make the case for it even better.

If you mean to be hook points for JavaScript polyfills, no. That is not their primary design purpose. That's mainly the goal of worklets and the CSS Paint API (Houdini).

JavaScript being able to interact with CSS variables so easily seems like it was intentionally designed and supported even if it isn't their primary design purpose for existing. Right now CSS variables have a lot more browser support than the Paint API, so they seemed like a natural building block to begin exploring the concept and fleshing it out.

If you wanted to experiment with totally custom syntax, CSS variables would still be a way you could include your custom syntax inside CSS in a valid way and handle parsing and processing of it outside of CSS while a feature like this was being worked on to help you avoid having to write invalid CSS.

Even after a feature like this is added to CSS and begins to have browser support, at the time you want to start using it you'll probably need a frontend polyfill to support this feature in browsers that can't parse it, so the polyfill you would write in the future to support the feature in older browsers might not look so different than what a speculative prototype you build for this feature might look like today. I think it's worth thinking about.

How would you prototype functionality like this to build a demo of it instead? I'd love to see a prototype of this idea working if you can implement it in a different way to test it out!

@matthew-dean
Copy link
Author

That's why I'm so confused by your reaction to this. I would have thought somebody wanting this feature like this would be excited about a demo, even if it's rough, because it proves something about the idea.

Sorry I probably misunderstood and thought you were derailing. I should have asked for more clarification. My apologies.

@tabatkins
Copy link
Member

Obviously, we already have min max, but what's useful about if() is that, like toggle(), you can use it for any value.

This is, unfortunately, what makes this so much harder, and probably not capable of happening. In your example, you seem to be assuming that 100% would be resolved as if it were a width value; I'm not sure why this should be the default.

Later, you talk about an alternate syntax that explicitly references other properties; this, unfortunately, means we're adding arbitrary dependencies between properties, something we've avoided doing so far because it's, in general, unresolvable.

Custom properties can arbitrarily refer to each other, but they're limited in what they can do, and have a somewhat reasonable "just become invalid" behavior when we notice a cycle. Cycles are more difficult to determine for arbitrary CSS, and can happen much more easily, because there are a number of existing, implicit between-property dependencies. For example, anything that takes a length relies on font-size (due to em), and so you can't have a value in font-size that refers to a property that takes a length (so no adjusting font-size to scale with width!). We add new dependencies of this sort over time (such as adding the lh unit, which induces a dependency on line-height); if authors could add arbitrary dependencies, we'd be unable to add new implicit ones for fear of breaking existing content (by forming cycles that were previous valid and non-cyclic).

toggle() gets away with its behavior because it's non-cyclic by definition, and relies solely on a dependency that already exists - the property's inherited value. This, unfortunately, has no such escape-hatch.

So, unfortunately, we've had to reject ideas like this in the past, and as far as I can tell, there's nothing mitigating the problems in this proposal either. Sorry. :(

@matthew-dean
Copy link
Author

matthew-dean commented Dec 20, 2018

@tabatkins

So, unfortunately, we've had to reject ideas like this in the past, and as far as I can tell, there's nothing mitigating the problems in this proposal either. Sorry. :(

That's alright! And I would assume you would know the engineering challenges better than I would. I have to do the same rejections for Less when it's just not feasible with how statements/functions/vars are evaluated. I didn't have high expectations for if() natively but thought I would ask.

Has there ever been a proposal / consideration of being able to define units, or capture values at a certain stage? That would maybe take CSS authors halfway.

As in (this syntax is terrible, just illustrating the concept):

.grandparent {
  width: 200px;
  height: 200px;
}
.parent {
  @unit --foo 100ct;  /* container height at this point, i.e. 200px */
  width: 1px;
  height: 1px;
  position: absolute;
}
.grandparent .parent .child {
  position: absolute;
  width: calc(1--foo);  /* use an inherited unit value, i.e. make .child same width as .grandparent */
}

That is, if there was a way to query the cascade, or mark values (declared or inherited) in the cascade with vars to consume later, it would provide a lot of dynamism without logic switches.

@matthew-dean
Copy link
Author

Incidentally:

so you can't have a value in font-size that refers to a property that takes a length (so no adjusting font-size to scale with width!)

You can adjust font-size to scale with width, as long as width is the viewport width. For instance, I have in a project where I do something like:

html {
  font-size: calc(0.625vw + 12px);

  @media (max-width: 479px) {
    font-size: 14px;
  }
  @media (min-width: 1260px) {
    font-size: 20px;
  }
}

And then, for most of my UI, it looks like

.title {
  font-size: 1rem;
  padding: 3rem;
}

So font-size scales with width (within a range), and then UI scales with font-size. This isn't totally component-friendly, of course, since the missing needed values you need there are container width / height, not viewport. (Rarely is vw/vh or @media queries what someone actually needs, but they work for limited applications.) But yep, caveats aside, you can tie font-size to width!

@tabatkins
Copy link
Member

as long as width is the viewport width.

Yes, because viewport width can't be specified (in ems or any other unit!), so there's no dependency to worry about. ^_^ I was referring to the width of an element in my comment (because that's the sort of thing you were referring to in your examples).

Has there ever been a proposal / consideration of being able to define units, or capture values at a certain stage? That would maybe take CSS authors halfway. [snip example]

There've been proposals, but inserting dependencies between elements in layout is just as fraught as dependencies between properties in the cascade. We very carefully design the layout algorithms to be solvable based on specific pieces of layout information being passed up and down the tree; if arbitrary layout information can move around the tree and influence other properties, we suddenly have a vastly more complicated situation to deal with. In the limit (which you hit pretty much immediately), we have to abandon the layout algorithms as written completely, and instead rephrase everything in terms of a constraint solver, letting the arbitrary new layout dependencies introduced by the author just fall out as they may. Arbitrary constraint solvers are, unfortunately, much slower in the general case than the layout algorithms are, so doing so would slow down all pages as well.


Tangent: be careful with your units invention - ch is already a valid unit, so I was really confused by your example for a while. ^_^

@matthew-dean
Copy link
Author

@tabatkins

Tangent: be careful with your units invention - ch is already a valid unit, so I was really confused by your example for a while. ^_^

lol whoops. Not a unit I've used.

In any case, if I didn't say it already, I'm excited to see min() max() and clamp() there. And toggle() was a nice surprise. So it's still great progress in my book.

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

3 participants