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] Re-adding min() and max()? #544

Closed
LeaVerou opened this issue Sep 28, 2016 · 40 comments
Closed

[css-values] Re-adding min() and max()? #544

LeaVerou opened this issue Sep 28, 2016 · 40 comments

Comments

@LeaVerou
Copy link
Member

Perhaps now that Values 3 has gone to CR it's time to reconsider adding min() and max() back into Values 4?

Authors are doing extremely weird hacks with calc() to emulate them in font-size and line-height, which they call "CSS locks". If you google CSS locks you will see how widespread these hacks are becoming, indicating a clear author need.

Furthermore, as CSS variables become more widely used, the need for min() and max() will become even greater. I recall some of the issues that made us drop them 6 years ago can now be solved with the invalid at computed value time concept that Variables introduced.

@dauwhe dauwhe added the css-values-4 Current Work label Sep 28, 2016
@Crissov
Copy link
Contributor

Crissov commented Oct 4, 2016

So, for “locks”,

selector {
  property: /* $min_value ≤ */ $fallback_value /* ≤ $max_value = $min_value + $max_increment */;
}
@media (min-width: $min_vw) and (max-width: $max_vw) {
  selector {
    property: calc( $min_value + $max_increment *   (100vw - $min_vw) 
                //*                               ———————————————————  */
                                                  ($max_vw - $min_vw)  );
  }
}
@media (max-width: $min_vw) {
  selector {
    property: calc( $min_value + 0 );
  }
}
@media (min-width: $max_vw) {
  selector {
    property: calc( $min_value + $max_increment /* = $max_value */ );
  }
}

would become

selector {
  property: /* $min_value ≤ */ $fallback_value /* ≤ $max_value = $min_value + $max_increment */;
  property: calc( $min_value + $max_increment * min(  (100vw - $min_vw) 
              //*                                   ———————————————————     */
                                                    ($max_vw - $min_vw), 1) );
}

right?

I wonder if authors would also find good use for mid()dle/avg(), med()ian, mod()e etc.

@MadeByMike
Copy link

I proposed a map() function that allows for min and max sizes as well as easing and interpolation of more than just length units: #581

I think this would be a much more versatile approach, especially with things like variable type, where min() and max() might not work.

@MadeByMike
Copy link

MadeByMike commented Oct 10, 2016

People have shared with me examples of how min() and max() functions in CSS might work in addition to a map() function. So to clarify, I don't think it needs to be one or the other.

Since calc() allows some min and max functionality (although not with whole numbers) It might be useful to consider other implementations of min and max including returning the min and max value of a set e.g. max(14px, 1em, 1rem, 3vw).

@LeaVerou
Copy link
Member Author

It's generally good API design practice to have shortcuts for commonly needed things, otherwise you end up with clunky, verbose APIs that are a pain to use for anything. I will not discuss whether what you're proposing is a good idea or not here, partly because I haven't thought about it much. However, even if it is, using map() to specify the min of two values results in extremely weird and convoluted code. Power is good, as long as it stays out of your way when you don't need it.

@Crissov
Copy link
Contributor

Crissov commented Dec 21, 2016

I wonder whether min() and max() would be the best notation, however, especially if these operations were only available within calc(). I know it’s unusual and incompatible with programming languages (which need logic operators and, unlike CSS, have a boolean data type), but maybe < and > (or << and >>) could be used as operators instead.

{
min(100px 10em 10vw)      calc(min(100px 10em 10vw));
min(100px, 10em, 10vw)    calc(min(100px, 10em, 10vw));
100px < 10em < 10vw       calc(100px < 10em < 10vw);
100px << 10em << 10vw     calc(100px << 10em << 10vw);

max(100px 10em 10vw)      calc(max(100px 10em 10vw));
max(100px, 10em, 10vw)    calc(max(100px, 10em, 10vw));
100px > 10em > 10vw       calc(100px > 10em > 10vw);
100px >> 10em >> 10vw     calc(100px >> 10em >> 10vw);
}

This works well for minimum and maximum where you can compare one pair after the other – CSS has only + not sum() despite grammar issues with the former. It would be more complicated to spec for other statistical functions but straightforward for some other operations, also the character repertoire (i.e. Latin 0 punctuation marks) is limited:

{
calc(a | b);	/* median */
calc(a = b);	/* arithmetic mean, average */
calc(a # b);	/* either count or mode (work) or geometric mean (doesn’t work) */
calc(a ~ b);	/* round $a to an integer multiple of $b */
calc(a _ b);	/* floor, round down $a to an integer multiple of $b */
calc(a ` b);	/* ceiling, round up $a to an integer multiple of $b */
calc(a ^ b);	/* $a to the power of $b */
calc(a % b);	/* $a modulo $b, or $a integer-divided by $b, or remainder of $a/$b */
calc(a : b);	/* one of the other from the line above */
/* … */
}

@LeaVerou
Copy link
Member Author

I believe we can't add angle brackets for some syntax reason, which is why we used min- and max- in media queries. But even if we could, your proposed syntax does not look more clear to me at all, and I don't understand why it would only be usable within calc(), there's no reason to not allow it bare. Furthermore, I don't think the other operators are more clear either, both to non-programmers and programmers alike, for different reasons. We don't need more ASCII art in CSS. And the modulo operator being % is a huge mistake IMO, since % has a clearly defined role in language, which is completely different than modulo.

@cvrebert
Copy link
Member

I believe we can't add angle brackets for some syntax reason, which is why we used min- and max- in media queries.

Erm, MQ4 uses angle brackets: https://drafts.csswg.org/mediaqueries-4/#mq-range-context

@Crissov
Copy link
Contributor

Crissov commented Dec 22, 2016

The reason for avoidance of < (in the past) was compatibility, not really syntax, as far as I remember. I don’t fancy % as a binary operator either, but it has precedent elsewhere.

The advantages of > over max() are really the same as for + vs. sum(), except that plus is much better established elsewhere. Semantically, > continues with the larger of the adjacent terms, whereas max() selects the largest out of a list. I prefer the latter in programming languages and spreadsheet applications, but, unlike those, CSS has only scalars to deal with – no vectors, ranges, arrays or objects (except perhaps for <<color>> eventually). We don’t need more JS notation in CSS.

@frivoal
Copy link
Collaborator

frivoal commented Dec 22, 2016

a > b does not read like the larger of a and b to me, but true if a is larger than b, false otherwise. Not just because that's what it means in many programing languages, but because that's what it means in math. I'm not favorable to this notation at all.

@Crissov
Copy link
Contributor

Crissov commented Dec 23, 2016

In mathematics, though, < and > are usually only used as relation symbols and not as operators. Outside MQ, CSS does not use logic expressions, unlike pretty much every programming language.

Pairing the angle brackets, i.e. >min< and <max>, also kinda works for me.

{
    max(a b c)    calc(max(a b c))
    <a b c>       calc(<a b c>)
    a > b > c     calc(a > b > c)

    min(a b c)    calc(min(a b c))
    >a b c<       calc(>a b c<)
    a < b < c     calc(a < b < c)

    abs(a)        calc(abs(a))
    |a|           calc(|a|)

    round(a, b)   calc(round(a, b))
    [a]b          calc([a]b)
    [a]~b         calc([a]~b)
    a ~ b         calc(a ~ b)

    floor(a)      calc(floor(a))          ⌊a⌋
    [a]_          calc([a]_)
    _a_           calc(_a_)
    (a _)         calc(a _)

    ceil(a)       calc(ceil(a))           ⌈a⌉
    [a]`          calc([a]`)
    [a]^          calc([a]^)
    `a`           calc(`a`)
    ^a^           calc(^a^)
    (a `)         calc(a `)
}

@MatsPalmgren
Copy link

Please use function syntax. Readability is much more important than saving a few key strokes.

@Crissov
Copy link
Contributor

Crissov commented Dec 23, 2016

Readability is my primary goal, too! It’s so easy to get lost in the parentheses of nested functions.

@frivoal
Copy link
Collaborator

frivoal commented Dec 23, 2016

To pretty much everybody who has ever seen that symbol, a > b means "a is larger than b" not "the larger of a and b", so I strongly object to giving it that meaning.

My objection against <a b> and >a b< is less strong, as there is no existing universally accepted meaning to it. I still don't like them though. max(a b) is much more guessable, much more googlable, easier to search in a a file that mixes markup and css...

@Crissov
Copy link
Contributor

Crissov commented Dec 23, 2016

It's a bit like default/fallback values in Lua etc.: Takes some time to get used to, but makes sense.

Anyway, I'm not feeling strongly about this, just wanted to provide some thinking outside the box.

@LeaVerou
Copy link
Member Author

Symbols and icons aid usability when they follow an established convention and are recognizable. If they have to be memorized and make no intuitive sense, then they just make the learning curve steeper. Think of those apps that use a long toolbar of icons that have no obvious meaning. I would be strongly opposed to any of these notations for this reason.

@plinss
Copy link
Member

plinss commented Dec 23, 2016

It can also be just about impossible to google a new syntax using symbols, making it hostile to novice authors.

@fantasai
Copy link
Collaborator

RESOLVED: Add min() and max() to css-values-, valid within and without calc(), accepts N calc() expressions as arguments.

@Crissov
Copy link
Contributor

Crissov commented Jan 12, 2017

I’m seriously considering to propose sum() or add() and sub() now, because that would also work outside calc().

mid()/avg(), med(), mod(); abs() etc. would probably also require separate issues.

PS: @LeaVerou is confusing lexicon and syntax of icons. My sub-proposal still had angle brackets meaning larger and smaller, it just took advantage of CSS’s feature of not needing a boolean type in property values – did someone ever propose if() or condition ? value : default for property values?

@fantasai
Copy link
Collaborator

One issue per report, @Crissov.

@fvsch
Copy link

fvsch commented Jan 13, 2017

Small question as an author: what would be the proposed syntax for authors declaring lower and higher bounds? Nesting min and max?

/* font size is a mix of rem and viewport-based dimensions,
   with 1rem as lower bound and 5rem as upper bound */
h1 { font-size: min(1rem max(5rem calc(.5rem + 2vw + 2vh))); }

I looked up the map() issue to see if it allowed this in a slightly more readable way, but failed to understand the proposal.

@bradkemper
Copy link
Contributor

@fvsch I don't like that much nesting, but how about this (proposed syntax enhancement):

h1 { font-size: calc(.5rem + 2vw + 2vh, min(1rem), max(5rem)); }

Or this:

h1 { font-size: calc(.5rem + 2vw + 2vh, minmax(1rem, 5rem)); }

Or maybe even this:

h1 { font-size: calc(.5rem + 2vw + 2vh, 1rem, 5rem); }

If you didn't want a min, you could put a zero in that spot. If you didn't want a max, you could just leave it off, along with its preceding comma.

@dbaron
Copy link
Member

dbaron commented Jan 13, 2017

The proposed syntax is that min() and max() are functions. min() results in the smallest of its (1 or more) arguments, and max() results in the largest of its arguments.

One of the issues with adding a minimum and a maximum constraint to a value (as in #544 (comment)) is that there are two options to how to process them, which produce different results when the minimum is greater than the maximum. CSS generally treats the minimum as winning, so that the standard CSS way to enforce a minimum and maximum constraint on a value is max(minimum, min(maximum, starting value)). However, if you want, you can also write min(maximum, max(minimum, starting value)) which would do the opposite when minimum > maximum.

But, in general, the functions allow you to combine values in other ways. For example, you can write things like max(500px, 20em, 30vw) which would result in the largest of the three arguments.

The further syntax proposal for allowing these to appear at top level is that, while these are functions that conceptually live inside of calc(), you're allowed to abbreviate calc(min(500px, 30vw)) by dropping the calc() on the outside and just writing min(500px, 30vw). min() and max() become toplevel functions that are allowed anywhere calc() is allowed, and just mean calc(min()), etc. This was the way I structured the original min() and max() proposal when I initially proposed calc().

@MadeByMike
Copy link

MadeByMike commented Jan 14, 2017

@bradkemper I really like your proposed syntax however I don't think it would give complete control over the rate of scale. You would not be able to pick the viewport range.

I wrote a whole lot more of my ideas relating to this discussion in an article: https://madebymike.com.au/writing/interpolation-without-animation/ and updated the map proposal.

@gabriel-peracio
Copy link

One could also add clamp() which would take three parameters value, startofrange and endofrange.

What this function does is return value if value is inside the range, and if not, then returns either startofrange or endofrange, whichever is closest to value.

examples:

width: clamp(500px, 10px, 999px); //returns 500px
width: clamp(1px, 100px, 9999px); //returns 100px
width: clamp(999999999px, 10px, 1000px); //returns 1000px
width: clamp(1px, 99999px, 10px); //returns 10px
width: clamp(1px, 99px, 99px); //returns 99px

It doesn't matter whether startofrange > endofrange or startofrange < endofrange or the trivial case of startofrange = endofrange (in which case it just returns startofrange and doesn't even check the value).

The behavior works slightly different when clamping angles, though, so beware that.

@Crissov
Copy link
Contributor

Crissov commented Aug 11, 2017

Isn't that just a shorthand notation?

clamp(value, start, end) = min(max(value, start), end) =max(start, min(value, end))

@tomhodgins
Copy link

I'll +1 this, I have a clamp() helper function I use when building designs sometimes, my JS code for that is pretty simple:

function clamp(min, mid, max) {
  return Math.min(Math.max(min, mid), max)
}

Example: https://codepen.io/tomhodgins/pen/ALWaVr

After experimenting around with this sort of clamping functionality — where you give a preferred (usually scalable) unit in the middle, capped on either end by a lowest and highest limit — it has been really useful for typography. I'd love to see a clamp() function in CSS, especially if it could be used for any value on any property!

@tabatkins
Copy link
Member

I do agree that clamp(), while technically superfluous, is useful often enough that it probably deserves to exist. (We use it a lot in the specs, after all!)

It also gives you a consistent answer to which wins if the two aren't in proper order, min or max - we'd match standard CSS practice and have min win. (If you want max to win, you can manually write a nested min()/max() expression with max() on the outside.)

I also agree that the argument order (min, mid, max) is best here, rather than (mid, min, max) - it matches what I use in JS too. ^_^

Finally, it avoids the significant confusion (at least, that I experience) when using min()/max() directly, where you have to put the upper value in the min() function, and the lower value in the max(). That is, you implement a min-* effect by using the max() function, and vice versa. This always confuses me! clamp() avoids that when you need both ends, and if this issue confuses you in general, you can just always use clamp(), especially if we explicitly allow infinity as a max argument (and -infinity as a min).

@gabriel-peracio
Copy link

gabriel-peracio commented Aug 14, 2017

The differences with clamp() are

  • start and end don't have to correspond to min and max, they can be "out of order" (so you don't have to invert them when doing calculations with negative values, for example)
  • start does not take priority over end, nor vice versa. The point closest to the provided value wins.
  • has different behavior when clamping angles (where it clamps to the smallest arc) or colors (no idea what it would do here but someone can come up with an idea)

The names can be changed, maybe start, mid and end so as to not give the impression that only one of them can be dynamic.

@tabatkins
Copy link
Member

out-of-order clamping (that is, interpret the smaller one as min and larger one as max, regardless of how they were specified) can definitely be done, but it doesn't match current CSS behavior of min/max pairs. Like "max wins", it can be done manually (where you see the minvalue in the normal expression, instead use min(minvalue, maxvalue), and same with maxvalue and max()), and might be worthwhile to expose with flags.

(Alternately, just waiting for custom functions could let us handle this. Since this can be done as a simple rewrite of the expressions, it might look something like:

@custom-function 
  --clamp-close($val, $min-val, $max-val)
  clamp($val, min($min-val, $max-val), max($min-val, $max-val);

)

Angle clamping can potentially be given special behavior, but can you elaborate on what your preferred behavior is? I can't understand it from your brief description.

Color clamping is a whole other kettle of fish, with lots of different behaviors one might want to use.

@gabriel-peracio
Copy link

gabriel-peracio commented Aug 15, 2017

If you give two random angles, there are two possible arcs described. Unless the angles are diametrically opposite (in which case I guess the function would simply return val without clamping), there should be a big arc, and a small arc.

For instance, if you pass 0° and 90°, there are two arcs: one is 90° and the other 270°.

Thus, when clamping an angle, say you just passed as parameters 0° for the start, 90° for the end and 110° for the val. Surely you want the function to return 90°, even though 110° is within the greater arc.

This seems intuitive, and behaves just like the previous example, until you look at things like passing 270° and 0°. You still want to clamp to the the smaller arc of 90°, not the greater arc of 270°. Therefore, passing 110° as val would return 0°, since that is what is closest to being within the arc bounds (you only have to turn 110° to get to 0° as opposed to the 160° you'd need to get to 270°).

Things also get confusing when you start providing things like 3600° (or any value bigger than 360°, really), but the trivial solution is to just mod it to 360° which would make 3600° equal to 0°. Negative angles also get trivially converted to their positive counterparts.

Alternatively there could be an angleclamp() function so that people aren't confused by special behavior (for instance start: 2700°, end: 3600°, val: 10° returns instead of 2700° which might be what they are expecting with clamp). A variant that clamps to the greater arc is possibly desirable as well but I haven't found a usecase for it.

@gabriel-peracio
Copy link

As for color clamping, I guess one could just use RGB() and clamp inside the parameters for maximum control

@tabatkins
Copy link
Member

For instance, if you pass 0° and 90°, there are two arcs: one is 90° and the other 270°.

That's if you treat the angles as just points on a circle, where spinning 360deg gives you the exact same point. We don't do that in CSS - angles are numbers like anything else; 0deg and 360deg might map to the same direction in the end, but they're treated as completely distinct values otherwise. (So you can, for example, animate from rotate: 0deg; to rotate: 360deg; to make something do a complete spin.)

So in CSS, if you give 0deg and 90deg, there's only one "arc", going from 0 and 90. Between 0deg and 270deg there's only one "arc", going from 0 to 270. If you want the "small" arc in that second case, use the angles property: specify -90deg and 0deg, or 270deg and 360deg.

This does suggest that there might be justification for modding angles, for when 0deg and 360deg really are exactly the same for a given application. But it shouldn't be built into clamp().

@zcorpan
Copy link
Member

zcorpan commented Sep 4, 2017

Should this be closed, given 371d0a1 ? @tabatkins

@tabatkins
Copy link
Member

Still needs a testcase.

Also I'd like to split out the clamp() proposal to a separate thread.

@dennisgaebel
Copy link

@tabatkins Is there a thread for the clamp() proposal as of today?

@tomasdev
Copy link

just gonna throw this here:

font-size: min(20vw, 10vh)

@ewilligers
Copy link
Contributor

There are implementation implications. Currently, implementations can internally reduce any calc to an array of coefficients. (Element dimensions, font size, device orientation might change during an animation, so a calc can't simply be represented as a number of pixels.)

This proposal requires a slow path of general expression evaluation during each frame's style calculation.

Suppose we have animation between min(10%, 20vw) and max(30em, 40vh).

At 75% progress, the result is
calc(0.75 * min(10%, 20vw) + 0.25 * max(30em, 40vh))

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 27, 2018

@ewilligers Can’t this still be reduced to an array of coefficients, i.e. calc(0.75 * (a * 10% + (1-a) * 20vw) + 0.25 *(b * 30em + (1-b) * 40vh) where a, b ∈ [0, 1] ?

If a,b change during the animation, you can pre-calculate that.

@tabatkins
Copy link
Member

That's a nested structure, which can be arbitrarily nested, not a flat array. (Aka a sum of simple values.)

But we already need to support this sort of arbitrary structure for the unit algebra that the WG resolved on and which is already specced in Typed OM.

@felds
Copy link

felds commented Mar 6, 2018

@tomasdev I don't get it…

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