Open tabatkins opened 2 years ago
I tried to find reasons why that would be annoying to implement, but can't think of any. So ... sounds good?
Like the Idea!
I always think that the behaviour of custom properties should (possibly) not differ from normal properties. That's why I'm throwing these lines into the room:
.el {
padding-top: 2rem;
padding-bottom: 2padding-top;
}
That however, is complicated, because it adds new and arbitrary dependencies between things that previously couldn't depend on each other.
This also opens up the way to supporting new units through a polyfill:
Alias the original unit to its --
counterpart, i.e.
@property --brm {
syntax: "<length>";
initial: 1brm; /* browsers with support will use this */
inherits: true;
}
Have a JS polyfill calc + set the initial value in case of no support
Use the custom unit throughout the code
height: 100--brm;
Received a reply on Twitter where the author noted that they find the syntax confusing
6--fr
doesn’t read like6fr
to me, it reads like6 - -fr
and now I’m wondering what fr resolves to, before realizing what this means
I think it's a matter of getting used to it. Once you know how it works, it's OK to read imo.
To be honest I was going to make the same point. calc(4 * var(--x))
I can look at and understand without a spec - not so with4--x
. I expect it's even worse if you don't know the details of the CSS tokenizer.
Sure I can get used to it, but my first reaction is it's a slightly shorter but considerably less intuitive alternative syntax for something we can already do - increasing cognitive load to save a few characters. Clarity wins over brevity for me, so I don't think it's an improvement.
Yeah, readability is an issue, but we had the same concerns about custom properties in general at first, and it seems like that was indeed fine once your eyes got used to it. (After all, in CSS spaces are required around subtraction anyway.) At least we're guaranteed these aren't confusable with built-in units 😃
However, I don't believe this is "slightly shorter" - it's hugely shorter. In raw characters it's a difference of 14 characters per use, including two pairs of parentheses. It's also not immediately distinguishable from more complex math (particularly when embedded in a larger math expression), so you have to parse it manually and realize it's just scaling a variable. That's a big cognitive, visual, and typing load for something that's meant to extremely simple and common.
The CSS Working Group just discussed custom units as variables
, and agreed to the following:
RESOLVED: Start new draft of variables-2 and add custom units as described here
As defined here this might be helpful for design system helpers like [the 8pt grid](The Comprehensive 8pt Grid Guide. Start your UI project right with this… | by Vitsky | The Startup | Medium).
With the ability to inject the custom unit increment into the calc function you can start to do more like adding modular scales to CSS, or creating a complex clamp()
function.
What if it looked something like this:
@unit --scale { /* Changing this from @property to something more specific avoids the need for initial */
syntax: "<length>";
value: --value; /* This will be the input value */
formula: calc(1rem * pow(1.5, var(--value)));
}
h1 {
font-size: 4--scale; /* 5.0625rem */
}
You might also be able to do this to simplify clamp functions:
@unit --fluid {
syntax: "<length>";
value: --value; /* This will be the input value */
formula: clamp(1rem, 1vw * var(--value), 1rem * var(--value));
}
h1 {
font-size: 4--fluid; /* easier to implement clamp function */
}
There is more opportunity than syntax sugar for design systems 8pt grid that I think is worthy of exploration.
Adding a note from @jonathantneal via Twitter where he talks about adding some of the above functionality. I think it’s needed as it greatly expands the utility of custom units.
When I first pitched the approach, it did suppose a special unit to signify the original number.
It supposed an
x
unit, but that was taken. Perhaps it should have beenvar
.
@property --fluid {
syntax: "<length>";
initial: clamp(1rem, 1vw * 1var, 1rem * 1var);
inherits: true;
}
h1 {
font-size: 4--fluid;
}
To expand just a little on @scottkellum’s comment, I was wondering if we could utilize a ‘nesting’ unit for math, similar to how we might utilize a nesting selector for rules.
<dimension>
.var
, n
, whatever
— is bike-shedding.<number>
.
--step: calc(1rem / .25n)
would be equivalent to --step: calc(1rem / .25)
.12--step
would be equivalent to calc(1rem / (.25 * 12))
.This proposal leaves open the possibility for a more full-featured custom units proposal in the future (if you registered a custom --unit it would just win over a --unit property), but I'm explicitly not trying to do anything more complicated than simple variable substitution right now.
This is because simple variable substitution solves the 90% case, afaict, and getting any more complex starts to get really complicated. For example, if you do calc(1--unit + 2--unit)
, is the result equivalent to 3--unit
? In all the examples given here, it absolutely is not, which implies that you're not defining a "unit" at all, but rather a custom function of some kind. That's also something we should do (and has also been on my back burner for a long time, including a simple declarative substitution-based approach like what you're suggesting), but it's separate from the idea of a "unit", which needs to be a vector.
Put a slightly different way - the approach I'm taking (just multiply the value by the substituted variable) works for everything that acts like a "unit" should - can be added together, multiplied, etc. If you're wanting something that doesn't work under this approach, you're not wanting a "unit", but something more complex, and we should address that with a different method.
For example, with your --fluid example, that can mostly be done as:
@property --fluid {
syntax: "<length>";
initial: min(1vw, 1rem);
inherits: true;
}
This does not enforce the "no fluid lengths are ever allowed be less than 1rem
" condition in your version, because that's not something you can reasonably apply at the individual-value level - it means that, say, .01--fluid
and calc(1--fluid / 100)
are very likely not equal (the first is much larger, instead). What you want, instead, is a way to clamp a value at an author-specified time, in a short readable fashion.
Like, pretend for a moment that we have simple custom functions, like:
@custom-function --fluid(--value) {
arg-syntax: --value "<number>";
result: clamp(1rem, var(--value) * 1vw, var(--value) * 1rem);
}
This could work - you can say width: --fluid(5);
and get a reasonable result, and importantly, there is no expectation that calc(--fluid(1) + --fluid(2))
is equivalent to --fluid(3)
, or that calc(--fluid(1) / 100)
is equivalent to --fluid(.01)
. These expressions are reasonable to be different values, so you don't have the same issues as a "unit" does.
Thanks @tabatkins this makes sense!
When I think of modular scales I think of them like exponential rulers that map to my mental model of units but I can see how units need more interoperability than that mental model provides.
I like this custom function idea.
Not something against, but it will look a bit odd :)
:root { --fr: calc(1rem * .25); }
.usage { grid-template-columns: 1--fr 1--fr 1--fr; }
Custom units as syntactic sugar would be great. Custom functions would be awesome. It would allow us to build our own functions such as --progress
to calculate a fluid ratio percentage to use in the new mix()
function.
@custom-function --progress(--current, --min, --max) {
arg-syntax: --current "<length>", --min "<length>", --max "<length>";
result: clamp(0%, 100% * (var(--current) - var(--min)) / (var(--max) - var(--min)), 100%);
}
:root {
--fluid-ratio: --progress(100vw, 375px, 1920px)
}
.usage {
font-size: mix(--fluid-ratio, 1rem, 1.25rem)
}
Custom functions would allow us to write more readable css with less repetition.
@tabatkins Do you know if there are there any issues tracking custom functions?
There is not currently such an issue. Feel free to open one. ^_^
Thought of another use-case that I would use these for:
rem
as a reference to the user's chosen font-size preference, which means not adjusting the font-size on the root element.rem
as a site-specific font size that's determined relative to that default.I can't have both. The only way to have option 1 is to give up on option 2. At that point I have to use custom props & calc in order to reference a site-specific base font size. That size is often a clamp function combining rem
and vi
units, established on the body element to avoid overtaking the rem
. Using custom units would make this feel much more natural:
body {
--bem: clamp(1rem, 0.9rem + 1vi, 1.5rem);
font-size: var(--bem);
}
h1 {
/* font-size: calc(2 * var(--bem)); */
font-size: 2--bem;
}
.small {
font-size: 1rem;
}
Thought I’d mention the specific web components / design systems challenge discussed in #7613 hem
: font-relative unit, relative to host element font-size – this is a clear use case for custom units.
If you want to use font-relative values in a Web Component for a UI element meant to be reusable across many sites,
rem
only works when the size of the root element hasn’t been customised unexpectedly (so unreliable in web components).em
works but its compounding nature is too hard to keep track of (particularly when also used for spacing, not just font size).So – with custom units, I’d introduce a either a host-relative hem
or container-relative cem
(as proposed in this comment):
/* In web components scoped styles */
h2 {
/* font-size: calc(2 * var(--cem)); */
font-size: 2--cem;
}
/* In parent page styles */
html {
font-size: 62.5%;
}
my-component {
--cem: 1.6rem;
}
I do not have a personal interest for this feature (I came across this issue to report the following potential oversights) but it seems a bit unfortunate to me to introduce a slight inconsistenty in the way a custom variable is referenced: one would be able to specify width: 1--fem
but not color: --primary
.
The oversights/suggestions:
<custom-property-name>
may be preferable to <dashed-ident>
to avoid confusing values like margin: 2-- 1px
<dimension>
and the custom unit/**/
must be inserted when serializing a specified valuewhitespaces probably need to be disallowed between
<dimension>
and the custom unit
1--fem
is a single <dimension>
token with value 1
and unit --fem
.
It is not a <number>
token with value 1
and a <ident>
token with value --fem
.
right?
margin: 2--1px
would indeed be confusing, but I think authors can always find ways to name things confusingly.
even worse : 2--1
Yeah, a <dimension-token>
is a single token, not <number-token> <ident-token>
.
So width: 1 --fem
would be invalid just like width: 1 px
or width: 1/**/px
.
And the spec already says
[A variable unit reference] has identical effects and restrictions to using
var()
If a property contains one or more
var()
functions, and those functions are syntactically valid, the entire property’s grammar must be assumed to be valid at parse time.
But I agree that the spec should refer to <custom-property-name>
instead of <dashed-indent>
.
Right, sorry! The unit is parsed as an an ident sequence (string) and it must match <dashed-ident>
(or <custom-property-name>
, as suggested).
<custom-property-name>
may be preferable to<dashed-ident>
to avoid confusing values likemargin: 2-- 1px
Sure, that's correct. The name has to be a custom property name anyway, since it's referencing a custom property.
- whitespaces probably need to be disallowed between
<dimension>
and the custom unit
Others have already said this - the custom unit is part of the <dimension>
already. It's not a separate identifier.
- the spec should define that
/**/
must be inserted when serializing a specified value
Rather, it can't, because this isn't two tokens.
- the spec should define that a property including a custom unit is valid at parse time
Yes, it'll be defined as just a fancy way to have a variable reference, which gives this behavior by default.
I've had "custom units" on the back burner of my mind for years now, and never got around to working on them - they had enough question to answer that it seemed exhausting. This morning, tho, I saw a tweet by @jonathantneal exploring the idea of making them just be sugar over normal custom property usage, and... I think I love it?
The example they had:
That is, if we see a "custom unit" (aka a dimension whose unit is a dashed-ident), we just treat it as a variable reference (triggering the normal behavior of using a variable - the property is assumed valid, etc), and expand it at variable-resolution time into exactly that calc - given
N--foo
, producecalc(N * (var(--foo)))
.I think this was problematic in the past because there were questions of initial value, resolution-time behavior, etc., but afaict those are all answered now by just using a registered custom property. That is, if you've registered your property/"unit" as a
<length>
, then you can set it like--unit:1.2em;
and it'll resolve that into an absolute length immediately, inheriting as a px length. (Or, if you do want the unit to resolve at point of use instead of point of definition, just leave it unregistered, or registered with a*
grammar.)Plus, a property registration suffices to fully define the "unit" immediately, since you can just set its size in the
initial
descriptor. But it also leaves open the possibility of redefining it on the fly like any other custom property, if needed.So the above example could instead be set up as:
This still leaves the door open to do a more full-featured custom unit thing later if we want; full-featured "registered custom units" would just override the variable-based behavior instead. But for now I think this does 95% or more of what we want custom units to do, in a flexible and readable manner.
Thoughts?