Skip to content

[css-values-5] Maybe min, max and step should not be part of the random-caching-key #11742

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

Open
smfr opened this issue Feb 19, 2025 · 46 comments

Comments

@smfr
Copy link
Contributor

smfr commented Feb 19, 2025

Currently, CSS Values 5 says that the min, max and step are part of the random-caching-key.

However, this prevents some possibly useful techniques where an element shares the same random value, but with different min, max and step with another property on the same element, or with another element.

Conceptually, I would expect that the underlying 0-1 random number is sampled based only on the dashed-ident or per-element keyword, and then min, max and step are just math applied to that underlying value.

Consider

        .box {
            background-color: blue;
            height: random(per-element, 50px, 100px);
            width: random(per-element, 100px, 200px);
        }

I want this box to have a random size, but always a 2/1 aspect ratio. With the current spec, width and height get their own random values.

Another example might be that you have two elements that want a width based on a single random value, but they quantize that value in different ways.

@tabatkins
Copy link
Member

The issue is that if min/max/step aren't included, then it exposes details of exactly how you generate the random value.

To avoid that, I'd have to specify that the random value is, specifically, a 0-1 real, and then specify exactly how it's turned into the random value. (aka, scaled to the desired range, or converted to a 0-N integer for the step)

I definitely could do that, and it would avoid some other oddities (like 1em and 16px potentially being the same caching key). It just might slightly restrict how you can actually generate the random value a bit.

@fantasai
Copy link
Collaborator

fantasai commented Feb 19, 2025

I want this box to have a random size, but always a 2/1 aspect ratio. With the current spec, width and height get their own random values.

I think the more obvious way to do this would be to use the same random() function in both, like this: width: calc(random(per-element, 50px, 100px)*2).

Another example might be that you have two elements that want a width based on a single random value, but they quantize that value in different ways.

You can do the same thing for this: pull out the random() function and pipe it into a round() function.

I think these use cases are relatively rare, and it's more obvious what you're doing if you synchronize the random() functions.


I think what's more likely, actually, is that you want two random() functions with identical arguments to return different values. For example, maybe you want a bunch of randomly sized boxes on your page with their widths and heights between 10px and 100px:

width: random(10px, 100px);
height: random(10px, 100px);

But this will generate squares right now, right? It seems to me that requiring a shared name to tie them together makes more sense than requiring different names to differentiate. As you say, if you accidentally end up with two random() functions with shared used values right now, they'll get accidentally tied together.

The important thing is to make sure that random()'s internal randomness value is part of its computed value so that it inherits correctly; but having each unnamed instance be unique actually makes more sense to me than making identical arguments return identical values... unless they're explicit about wanting to repeat a value, people usually want more randomness out of their random()s, not less, right?

@smfr
Copy link
Contributor Author

smfr commented Feb 19, 2025

To avoid that, I'd have to specify that the random value is, specifically, a 0-1 real, and then specify exactly how it's turned into the random value. (aka, scaled to the desired range, or converted to a 0-N integer for the step)

That's how my brain expected it to work. But maybe that's programmer brain.

@smfr
Copy link
Contributor Author

smfr commented Feb 19, 2025

See also #11747, which would be resolved in the min/max/step were not part of the random key.

@tabatkins
Copy link
Member

These arguments are pretty convincing, and being able to knock out #11747 would be nice. However...

But this will generate squares right now, right? It seems to me that requiring a shared name to tie them together makes more sense than requiring different names to differentiate. As you say, if you accidentally end up with two random() functions with shared used values right now, they'll get accidentally tied together.

The issue with making a "default" random() (without an explicit caching key) assume a unique key is that it's not very clear to authors when such a key would be generated. For example:

.rectangle {
  /* these would be different random values */
  width: random(10px, 100px);
  height: random(10px, 100px);
}

.rectangle-2 {
  /* these would be different random values */
  --size: random(10px, 100px);
  width: var(--size);
  height: var(--size);
}

@property --size2 { syntax: "<length>"; inherits: true; initial-value: 0px; }
.square {
  /* these would be the *same* random value, making squares */
  --size2: random(10px, 100px);
  width: var(--size);
  height: var(--size);
}

To an extent authors already have this confusion, since --foo: 1em; has different inherited behavior for registered vs unregistered custom props, but this random() behavior would make it visible even before inheritance. (Technically the same behavior distinction also occurs for values like 1em, it just doesn't matter in practical terms whether 1em resolves in the custom prop or in the substituting prop.)

This is how I arrived at the current caching behavior - it can result in accidentally linked random values sometimes, but it also ensures that authors don't have to care about when the substitution occurs.

So, hm, tradeoffs either way. Maybe it is better to force authors to think about the variable resolution time in return for having "more random" behavior by default and avoiding the value-resolution dependence.

@tabatkins
Copy link
Member

tabatkins commented Feb 24, 2025

Ah, right, the other reason I made the current behavior is it reduces the coupling to the precise source document. That is, currently, you can have multiple rules applying the same declaration to an element, and it's the exact same behavior as only having one of them apply. With "implicit unique key", you lose this: having multiple stylesheets applying the same styles to an element will give different results. This exposes things like stylesheet deduping, which currently are undetectable from inside the page. It also exposes the difference between applying a style to multiple elements via a selector and via style attribute. And the difference between .foo, .bar { width: random(); } and .foo { width: random(); } .bar { width: random(); }. All of these things are indistinguishable today, and I'm betting a lot of tooling implicitly depends on this.

The current behavior instead makes the caching key based purely on the source text itself, not its location, so you can't tell whether the same value is applied from several rules or just one, or if a repeated stylesheet is duped or deduped, etc.

The accidental linking of functions that happen to have the same arguments is just an unfortunate side effect. I included as much data as possible in the key precisely to make this less likely to happen by accident.

@tabatkins
Copy link
Member

So just to distill this down, my pushback here is that, today, for all CSS values, the precise source location of a value doesn't matter. (Some things care about which tree context a value's stylesheet is in, but that's the extent of it.) So .foo, .bar { prop: value; } is exactly identical to .foo { prop: value; } .bar { prop: value; } in terms of observable effects, .foo { prop: value; } is identical to .foo:nth-child(even) { prop: value; } .foo:nth-child(odd) { prop: value; }, etc.

Having random() infer a caching key from its source location would break these equivalencies. I don't know that ya'll have fully considered this ramification, and how tooling might currently be implicitly depending on it. It also has weird interactions with unregistered variables, since its "source location" would be the real/registered property it's subbed into, instead.

The only way to avoid this is to make the random() value depend, in some way, only on its arguments. Some possibilities:

  • The current spec takes all arguments into account to decrease the chance of accidental coupling between independent values.

  • We could alternately just make it depend solely on the key argument. This would mean that all values are associated by default.

  • I know I originally considered making the default key dependent on the property it's used in. That might be a good middle ground. Still a little confusing in variables, but it means that the values will be associated when used in the same property and unassociated when used in others, which should usually be right. (Registered custom properties will still resolve the random() "early" so they'll be the same value everywhere.)

@fantasai
Copy link
Collaborator

fantasai commented Mar 5, 2025

Definitely random() shouldn't infer a caching key from its source declaration. But I think we can and should give it maximum randomness unless the author explicitly ties things together.

Randomness needs to resolve at computed-value time. That's required for it to inherit sensibly.

Given that, I think each random() instance (unless otherwise specified by the author) should compute independently. If you specify the same random() function twice in a single property on a single element, or on two different properties, or on two different elements, they would compute to different values. Essentially, if you don't specify a caching key, we throw in the property name, an index (this is the Nth random() in the[+] value, or maybe just generate a unique identifier), and the element identity as the caching key.

Then if the author wants to tie things together, they can do that explicitly. Two random() functions compute to the same value if they share an identifier, with per-element essentially subbing in the element's identity. So random(--foo, 1, 10) always computes to the same value across the entire page; and random(per-element, 1, 10) always computes to the same value on a single element, but different values on different elements; and random(per-element --foo, 1, 10) computes differently than random(per-element --bar, 1, 10).

Yes, that means registered custom properties behave differently because they compute things up front. But that's already true, as you point out.


The one other thing to be careful of is shorthand properties and omitted values. We probably want margin: random(1, 10) to resolve to the same random value on all four sides simply because if we don't, then splitting a property into longhands later becomes problematic. (Also it's a reasonable expectation for shorthands, to duplicate the value.) We can solve this by saying that random() is keyed before expansion.

@Loirooriol
Copy link
Contributor

If you specify the same random() function twice in a single property on a single element

So I think you are saying the default key should be a tuple of

@tabatkins
Copy link
Member

tabatkins commented Mar 5, 2025

Essentially, if you don't specify a caching key, we throw in a unique identifier and the element identity as the caching key.

This is still hiding the complexity. What, precisely, is the scope of the uniqueness? We need the ID to be stable across styling passes, and across the same style applied by different rules, but we also need it to be different across... some boundary. At minimum, between properties. We probably want multiple uses in the same property to be unrelated, too.

Ah, now that I'm trying to find some text I remembered seeing, I notice you were more specific in the original version of your comment in my email, but editted it to be more generic later. I think your original suggestion is indeed exactly what we'd have to do: construct a key from the property it's used in (recording the shorthand used, before expansion into longhands) + an index for how many random functions precede it in the value. (+ some extra entropy for the pageload itself, so different pages or different visits to the same page are unrelated, but that's already baked into the definition and applies even when a key is given)

It would be interesting if we could also bake in some entropy from the token stream itself, so using the same number of random()s on the same property, but changing the rest of the property value in some way, would also result in different values. However, that requires a very precise canonicalization of the value into some sort of bytes, and I don't know that it's worthwhile to do so. It should probably be fine to just stick with property+index; you usually won't notice the connection.

@tabatkins
Copy link
Member

Note that this does still mean that, if you don't use per-element, every first instance of random() on a particular property across the entire page will share the same random value. That might be a little surprising, but I don't think we can avoid that without violating the equivalencies I mentioned earlier.

Note this introduces more case where we will need to use internal values, raising the priority of #8055

...unless we specify that it's literally equivalent to specifying --margin-1/etc as the key. Then we can just reflect that directly. I don't see any particular reason that would be bad? It seems unlikely to clash with anything an author actually provides, and it lets them manually hook it if they do want to. (That's not necessary since they could just specify the key themselves and then use it elsewhere, but hey.)

@tabatkins
Copy link
Member

Closely related issue: I'm trying to make sure that custom properties work in an understandable way, both registered and unregistered.

Unregistered is easy, they're just a little weird. They don't even recognize that a random() function exists, so everything about them waits until they're actually substituted - both computation of the default caching key, and resolving the per-element value. A little tricky, but explainable.

Registered properties present an issue, however. They'll see the random(), so they can compute the default caching key based on the custom property. And if the computation is fully resolveable by computed-value time, they can even handle per-element based on the element the custom property is applied to. But they're substituted as token streams, still, so if the random() isn't fully resolved by inheritance time (it contains a %, for example), then it'll remain as random() and inherit as such, finally applying per-element on the element it's actually substituted into. That's weird! The behavior of random() shouldn't depend on its numeric arguments at all, and double shouldn't do so specifically only when used in a registered custom property!

I could hand-wave some hidden state that has to be carried with it, but I'd prefer avoiding that if at all possible. This suggests that the "random base value" that is computed at computed-value time needs to be explicitly exposed, so that the computed value of a random() or random-item() function is some other, non-random function, with the random value subbed in.

That is, at computed-value time:

  • random(..., A, B) computes to calc(R * (B - A) + A), where R is the 0-1 random value
  • random(..., A, B, by C) computes to calc(R * C + A)`, where R is the random int between 0 and however many C steps fit between A and B. (Or to the same as the previous bullet, if the range is big enough and C is small enough that it hits the "just ignore the step" behavior.)
  • random-item(..., A, B, C) just computes to A, B, or C, as appropriate.

We then rely on normal computed-value behavior from there, simplifying if possible. This would result in an observable calc() showing up in some cases for the computed value (rather than being either random() or a fully-resolved value, as in the current spec).

@tabatkins
Copy link
Member

Uggghhhhh, no, my second bullet doesn't work, because the R's range depends on actually evaluating A, B, and C. I need to phrase it in a way that lets me keep them unresolved.

Unfortunately, even if I'm more explicit about the random int calculation, it doesn't let me switch into the "infinite steps" error case; the calc() would instead hit some infinity degeneracies. Oof, and the first bullet point is bitten by that too - the calc() there also hits some infinite degeneracies and doesn't have the same argument range controls. Argh, I think I need to just let the first argument be a 0-1 number, in which case we just treat that as the random base value rather than computing one based on the caching options.

That is, random(.5, 100px, 200px) will just resolve to 150px. This syntax wouldn't be designed for authors, and generally wouldn't be seen by authors using random() except in the weird case I outlined above. We can even say it has a special keyword of its own that makes it clear it's not for author use, like random(ua-resolved-value 0.5, 100px, 200px) or something.

@tabatkins
Copy link
Member

Hm, just spitballing if we could handle this via calc-range() or whatever (the "interpolation" variety).

It would need the by C ability added to the between-stops interpolation qualities. But that's also a little strange - A+n*C doesn't necessarily hit B; the current random() spec handles this by just suggesting you write your B to slightly overshoot, and lets the actual end of the range be exactly an A+n*C value. We'd need to be able to round the endpoint down a little...

@function --step-range-limit(--a, --b, --c) {
  --dist: calc(var(--b) - var(--a));
  --step: calc(var(--dist) / var(--c));
  --rounded-dist: round(down, var(--dist), var(--step));
  result: calc(var(--a) + var(--rounded-dist));
}
@function --step-count(--a, --b, --c) {
  --dist: calc(var(--b) - var(--a));
  --step: calc(var(--dist) / var(--c));
  result: calc(1 + round(down, var(--dist) / var(--step), 1));
}

/* then `random(A, B, by C)` computes to... */

calc-range(R,
    0: A,
    by steps(--step-count(A, B, C), jump-none),
    1: --step-range-limit(A, B, C)
)

Tho, no, sigh, that still doesn't get me some of the argument handling. I guess I will just need to make random() handle this.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

Ah, now that I'm trying to find some text I remembered seeing, I notice you were more specific in the original version of your comment in my email, but editted it to be more generic later.

Heh, amazing. Reverted to an earlier version.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

Note that this does still mean that, if you don't use per-element, every first instance of random() on a particular property across the entire page will share the same random value.

No they won't, because we're also including the element identity by default in the caching key.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

It seems unlikely to clash with anything an author actually provides, and it lets them manually hook it if they do want to.

No, I don't think these identifiers should be author-exposed. If the author wants to tie things together, they should do it explicitly.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

But they're substituted as token streams, still, so if the random() isn't fully resolved by inheritance time (it contains a %, for example), then it'll remain as random() and inherit as such

Like I said, the computed value of a random() function needs to have resolved the randomness. Otherwise inheritance is broken for everything, nevermind variables.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

We can even say it has a special keyword of its own that makes it clear it's not for author use, like random(ua-resolved-value 0.5, 100px, 200px) or something.

I don't see this keyword as helpful. We either just allow the number directly, we compute this to a -map() function, or we say that getComputedStyle() returns the unresolved random and assigning the value back gets you a new random. (I don't think this would be entirely unexpected, tbh.)

(Tangential point: we should make sure map() and random() syntaxes are aligned.)

@tabatkins
Copy link
Member

No they won't, because we're also including the element identity by default in the caching key.

Ah, I missed that, ok. Sounds reasonable.

No, I don't think these identifiers should be author-exposed. If the author wants to tie things together, they should do it explicitly.

Now that I've hit on the "just put the random number right in the function" solution, it's okay to make this value not author-exposed, so I agree.

or we say that getComputedStyle() returns the unresolved random and assigning the value back gets you a new random. (I don't think this would be entirely unexpected, tbh.)

That's not the issue, it's that a registered custom property (and any normal property containing a % that requires layout) containing random(100px, 200px) would resolve before inheritance and be the same value on all inheriting elements, but one containing random(10%, 20%) would remain unresolved and inherit as random(), thus getting new random values on every element it inherits to.

So we really do need to guarantee that we resolve the value in some way by computed value time.

(Tangential point: we should make sure map() and random() syntaxes are aligned.)

Nah, random() is much closer in spirit to, say, clamp() than map(), and it does match those other math functions nicely.

@fantasai
Copy link
Collaborator

fantasai commented Mar 7, 2025

So @tabatkins and I worked through this issue and our proposal is the following:

  1. As Simon proposes in issue, we would drop the min and max values from being part of the caching key.
  2. We clarify that the computed value of random() function, if cannot be fully resolved, includes the random base value (0-1) from which we index into the range. In order to make this serializable, we allow the random base value as a <number[0,1]> to be specified in place of the caching arguments in the random() notation.
  3. We make random() with no arguments "maximally random" by implicitly including the property name (which is sensitive to whether or not it's a shorthand), an instance index for that declaration (so that two random() functions within a given declaration will get different values), and the element identity into the caching key.
  4. We define that a named caching key replaces the property/index, so that random(--foo) functions applied to a single element can be tied together.
  5. We invert the previous per-element keyword into bikeshed-match (name TBD, Tab proposes match-element, I think that's confusing) which disables the per-element identifier so that random(bikeshed-match) values can be made to match across elements. (This keeps the property/index key.)
  6. We allow combining the custom ident and the new keyword, which allows authors to have globally-synced random() functions.

Example of the new caching rules outline above:

  • Maximum random: both properties get a different value, and it's different per element too, so you get lots of random rectangles.
    .random-rect {
      width: random(100px, 200px);
      height: random(100px, 200px);
    }
  • Shared by name within an element: both properties get the same value, but it's still different per element, so you get lots of random squares.
    .random-square {
        width: random(--foo, 100px, 200px);
        height: random(--foo, 100px, 200px);
    }
  • Shared between elements within a property: both properties get different values, but they're shared by every element, so you get lots of identical rectangles of a single random size.
    .shared-random-rect {
        width: random(bikeshed-match, 100px, 200px);
        height: random(bikeshed-match, 100px, 200px);
    }
  • Shared by name globally: both properties get the same value, and every element shares the random value, so you get lots of identical squares of a single random size.
    .shared-random-squares {
        width: random(--foo bikeshed-match, 100px, 200px);
        height: random(--foo bikeshed-match, 100px, 200px);
    }

We think this resolves all the comments and suggestions in this issue and in #11747. Agenda+ to discuss; edits in-flight to the spec for review.

@tabatkins
Copy link
Member

Edits checked in!

@Loirooriol
Copy link
Contributor

The per-element inversion seems confusing to me. The previous approach was more intuitive to me:

  • Shared by name globally: both properties get the same value, and every element shares the random value, so you get lots of identical squares of a single random size.

    .random-square {
        width: random(--global, 100px, 200px);
        height: random(--global, 100px, 200px);
    }
  • Shared within an element: both properties get the same value, but it's still different per element, so you get lots of random squares.

    .shared-random-rect {
        width: random(per-element, 100px, 200px);
        height: random(per-element, 100px, 200px);
    }
  • Shared between elements within a property: both properties get different values, but they're shared by every element, so you get lots of identical rectangles of a single random size.

    .shared-random-squares {
        width: random(--width, 100px, 200px);
        height: random(--height, 100px, 200px);
    }
  • Maximum random: both properties get a different value, and it's different per element too, so you get lots of random rectangles.

    .random-rect {
        width: random(per-element --width, 100px, 200px);
        height: random(per-element --height, 100px, 200px);
    }

    And then we can say that if the options are omitted, they they default to per-element followed by an ident consisting of --, the property name, --, and the index.

@nt1m
Copy link
Member

nt1m commented Mar 8, 2025

The match-element behavior feels inverted to me (at least when you compare it to the VT match-element). match-element uses the element identity as key in VT, in random() it stops using the element identity as key...

Perhaps it needs to be named element-global or something to describe better what it does.

I'd also be fine with @Loirooriol's approach.

@tabatkins
Copy link
Member

I inverted the keyword for two reasons:

  1. When writing the examples, the progression from "most random" to "most shared" now had a weird curve - omitting the key got you the most randomness, adding one value to the key suddenly flipped you to the least randomness, then adding two values put you back in the middle.
  2. The caching key has two components: a name (either manually specified or auto-generated), and an optional element ID (either present or null). This suggests a 2x2 matrix of possibilities, but only three of them were specifiable with the per-element keyword: auto name and ID, manual name and ID, or manual name and null. Inverting the keyword gave you all four.

The match-element behavior feels inverted to me (at least when you compare it to the VT match-element). match-element uses the element identity as key in VT, in random() it stops using the element identity as key...

In both cases it causes the style to match between some context-specific notion of elements - in this case, it causes elements to match their element-specific IDs together (setting them all to null, but that is indeed matching). It's also closely related in semantics to the match-parent keyword used by a few properties, which causes elements to all adopt the same value from their parent rather than use individual values. In this case, it's causing elements to all adopt the same random value from the style rather than use individual values.

I'm open to other names, as we said in the comment, but it needs to be something that clearly expresses the behavior to the author, rather than something that's meaningful only with respect to the internal mechanics of the caching key.

@nt1m
Copy link
Member

nt1m commented Mar 8, 2025

random(match-element, 100px, 200px) reads as "I want the random() function to match the element's identity" which does not express the idea that the value will be shared across elements.

Some ideas from AI that I somewhat liked: element-shared, all-elements, selector-shared , shared-across. Perhaps some shuffling around of those nouns and adjectives can find a good name :)

@Loirooriol
Copy link
Contributor

Loirooriol commented Mar 9, 2025

This suggests a 2x2 matrix of possibilities, but only three of them were specifiable with the per-element keyword: auto name and ID, manual name and ID, or manual name and null

I don't follow what auto name you are talking about. Do you mean per-element?

Anyways, it's actually more than 2x2 because of indexes. Are the grow and shrink ratios set to the same amount?

flex: random(match-element, 0, 1) random(match-element, 0, 1);

I guess they don't, in order to maximize randomness, and because (I believe?) you can specify a common ident if you want the same amount.

Behavior Original-based Inverted
Don't vary.
Share among elements, properties and indices
flex:
  random(--global, 0, 1)
  random(--global, 0, 1)
flex:
  random(--global match-element, 0, 1)
  random(--global match-element, 0, 1)
Vary with element.
Share among properties and indices
flex:
  random(per-element, 0, 1)
  random(per-element, 0, 1)
flex:
  random(--element, 0, 1)
  random(--element, 0, 1)
Vary with property.
Share among elements and indices
flex:
  random(--flex, 0, 1)
  random(--flex, 0, 1)
flex:
  random(--flex match-element, 0, 1)
  random(--flex match-element, 0, 1)
Vary with index.
Share among elements and properties
flex:
  random(--1, 0, 1)
  random(--2, 0, 1)
flex:
  random(--1 match-element, 0, 1)
  random(--2 match-element, 0, 1)
Vary with element, property.
Share among indices
flex:
  random(per-element --flex, 0, 1)
  random(per-element --flex, 0, 1)
flex:
  random(--flex, 0, 1)
  random(--flex, 0, 1)
Vary with element, index.
Share among properties
flex:
  random(per-element --1, 0, 1)
  random(per-element --2, 0, 1)
flex:
  random(--1, 0, 1)
  random(--2, 0, 1)
Vary with property, index.
Share among elements
flex:
  random(--flex--1, 0, 1)
  random(--flex--2, 0, 1)
flex:
  random(--flex--1 match-element, 0, 1)
  random(--flex--2 match-element, 0, 1)
/* or */
flex:
  random(match-element, 0, 1)
  random(match-element, 0, 1)
Vary with element, property, index.
Don't share
flex:
  random(per-element --flex--1, 0, 1)
  random(per-element --flex--2, 0, 1)
/* or */
flex:
  random(0, 1)
  random(0, 1)
flex:
  random(0, 1)
  random(0, 1)

So even though omitting the options implies the "weird curve" in the original-based approach, I think it's preferable because it directly maps to "varies with". The inverted option kinda maps to "shared with", but it's less straightforward, because it's not obvious that an ident shares among properties and indices but not among elements.

@tabatkins
Copy link
Member

The keyword can't mention selectors, as it's not about that - completely different style rules can still share the same random base. And because there's already another notion of "matching" being employed (by name), I think this probably has to include "element" in it to indicate that's the axis of matching.

Thus the current name match-element ^_^ Note that this keyword pattern (verb-noun) usually omits prepositions, so the relationship can be ambiguous with a naive reading; our goal is usually just to express the vibe.

There's another complicating issue here - if we went with a preposition-noun keyword pattern, like across-elements, then when actually used the eye is drawn to the function name so it reads as random across elements, which can invert the meaning from what's intended. This was used intentionally in the previous version: "random per element" was exactly the meaning I wanted to convey.

Hm, intentionally breaking that connection by starting with the noun might be workable, then. element-shared does sound pretty good in that case.

@tabatkins
Copy link
Member

tabatkins commented Mar 10, 2025

I don't follow what auto name you are talking about. Do you mean per-element?

The caching key has two pieces - a "name" and a nullable element ID. If you don't provide an ident, the name is auto-generated.

Anyways, it's actually more than 2x2 because of indexes. Are the grow and shrink ratios set to the same amount?

No, that's the "auto name" I was referring to. Property+index forms the name portion of the key, together. I don't think it's useful to think about property and index separately; neither the current nor previous text treated those as distinct things. The old text didn't pay attention to either; the new text pays attention to both.

I guess they don't, in order to maximize randomness, and because (I believe?) you can specify a common ident if you want the same amount.

Correct. The two instances have different auto names, so they end up with different values. You can manually specify a shared name if you do want to tie them together.

So even though omitting the options implies the "weird curve" in the original, I think it's preferable because it directly maps to "varies with". The inverted option kinda maps to "shared with", but it's less straightforward, because it's not obvious that an ident shares among properties and indices but not among elements.

Specifying an ident almost always reduces randomness; it's specifying a "shared with" semantic. (The only time it can actually increase randomness is if you specify different idents between two style rules setting the same property; they'd otherwise generate the same auto name and thus share values.) So it matches with the behavior of the keyword; they both increase sharing.

In the previous formulation both the keyword and the ident reduced sharing. Previously, omitting an ident just used a "blank" ident, so every instance that didn't specify a name was implicitly shared with every other unspecified name. So specifying a name reduced sharing, and the keyword reduced sharing.

[a bunch of examples]

Your final "old" example is wrong. random(0, 1) is maximum shared, it's the most different from random(per-element --flex--1, 0, 1) that's it's possible to be.

Overall you seem to be really overcomplicating this, tho. ^_^ The name indicates how things are shared; you can choose to reuse a name across properties, across indexes, or across any other dimension. There's no need to think about those dimensions separately.

@tabatkins
Copy link
Member

tabatkins commented Mar 10, 2025

Back to keywords: how about we just go simple and use global? I think random(global, 0, 1) or random(--foo global, 0, 1) correctly suggests that every instance of that function across your entire stylesheet will share the random value - it's a "global" random rather than as much of a "local" random as we can make it.

Edit: No, nevermind, brain fart. While random(--foo global) is basically a global shared value, random(global, 0, 1) is definitely not - flex: random(global, 0, 1) random(global, 0, 1) will produce two distinct values in the same property, even.

@Loirooriol
Copy link
Contributor

Your final "old" example is wrong.

It's the "weird curve" thing. Which sure isn't great, but I'm not convinced that the alternative is any better.

The name indicates how things are shared; you can choose to reuse a name across properties, across indexes, or across any other dimension.

But not "across elements" dimension. Doesn't seem obvious to me.

I think it might be beneficial if some CSSWG member who has lots of followers on Twitter (or BlueSky or whatever people use nowadays) creates a poll asking developers what behavior they would expect.

@tabatkins
Copy link
Member

It's the "weird curve" thing. Which sure isn't great, but I'm not convinced that the alternative is any better.

I'm not sure what you're talking about here. In the old version, random(0, 1) and random(per-element --flex--1, 0, 1) are the exact opposite. Your table lists them as alternatives, which is wrong.

@Loirooriol
Copy link
Contributor

@Loirooriol in #11742 (comment)

And then we can say that if the options are omitted, they they default to per-element followed by an ident consisting of --, the property name, --, and the index.

@tabatkins in #11742 (comment)

When writing the examples, the progression from "most random" to "most shared" now had a weird curve - omitting the key got you the most randomness, adding one value to the key suddenly flipped you to the least randomness, then adding two values put you back in the middle.

So yeah they look like opposites but "completely random" is desirable in a short way, and "completely shared" can be opted in with just random(--global, 0, 1). So it seems more useful to make the case with omitted options be completely random.

It's a matter of whether it's more confusing to have a special behavior for no options which is the opposite of what you would normally expect, vs the inverted model where a keyword implies sharing across all dimensions except (for no immediately obvious reason) across elements.

@tabatkins
Copy link
Member

I still don't understand what you're saying. The "original" cell in your last row is wrong. If by "original" what you mean is "something new that I'm making up, that resembles the original design", you should say that instead.

@tabatkins
Copy link
Member

But not "across elements" dimension. Doesn't seem obvious to me.

Elements are never a dimension addressable from style (only from selectors, with careful coordination with your HTML) So if we want to be able to distinguish "shared between elements" and "unique per element" it needs a special-purpose keyword, not something controlled by the author. When element identity is potentially significant in other properties, we similarly handle it with special-case behavior.

And, since we're going with random(0, 1) defaulting to "unique per element", we need the keyword to opt you into the opposite behavior (shared between elements). Previously it was inverted - random(0, 1) was "shared between elements", and the keyword opted you into the opposite behavior (unique per element).

Having the default behavior be "unique per element" and the keyword being "unique per element" is inconsistent. It means leaving off the keyword has opposite behavior depending on other values - random(0, 1) is unique per element but random(--foo, 0, 1) would be shared between elements. That's pretty bad.

It also means there's no way to say "automatically vary by property/index, but match between elements", unless we add more syntax to let you manually opt into that behavior. In your table you glossed over this by instead manually using idents to recreate the behavior, but the point is that there's a useful automatic behavior you get by omitting the ident, and you should be able to get that regardless of the independent choice of unique/shared by element.

It's a matter of whether it's more confusing to have a special behavior for no options which is the opposite of what you would normally expect, vs the inverted model where a keyword implies sharing across all dimensions except (for no immediately obvious reason) across elements.

It is almost always the case that omitting something acts consistently regardless of what other values you specify or omit; if we have any exceptions they're super special cases. This extends all the way to "omit everything" when that's possible - this almost always gives the same behavior, for each omittable thing, as omitting it. Breaking that pattern should only be done with a very good reason.

So yeah, it's way more confusing to have a special beahvior for "no options" which is the opposite of what you'd normally expect.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-values-5] Maybe min, max and step should not be part of the random-caching-key.

The full IRC log of that discussion <fantasai> Proposal at https://github.com//issues/11742#issuecomment-2707697957
<emilio> TabAtkins: defining randomness in CSS because the execution model is not strictly temporal
<emilio> ... can't hold on to existing values
<emilio> ... don't want to calculate a random value on every recalc
<emilio> ... you need to have some stability
<emilio> ... the old draft did this via a caching key approach
<emilio> ... so if the key is the same between two instances of a random() function then the values need to be the same
<emilio> ... this caching key has changed tho
<emilio> ... author could provide a custom-ident, but also min/max/step values were pulled in
<emilio> ... any other random function would probably have a different step and thus generate a different random value
<emilio> ... this worked reasonably well but did mean that values could be accidentally linked together
<emilio> ... so random width/height you would get a random square
<emilio> ... rather than a random value
<fantasai> s/value/rectangle
<emilio> ... fantasai proposed something else, which is on the spec right now. Caching key is (property, index among random values in that property)
<emilio> ... so if you use `width: ...;` the key is (width, 0)
<emilio> fantasai: you don't want a global random
<emilio> ... caching key also includes the element identity
<emilio> ... all of that gets computed to a random value which they inherits so that we don't recalc it for inheritable properties
<emilio> TabAtkins: so on a single element if you use the same values on different properties you're guaranteed to get distinct values
<emilio> ... you can override it if you want, so you could still pass the same ident to width/height
<emilio> ... some good examples of this are in the spec
<emilio> ... don't know what people might need for this but I propose we accept the new draft based on elika's proposal for the different key
<emilio> q+
<emilio> ... one further question about bikeshedding the keyword name
<weinig> q+
<astearns> ack emilio
<fantasai> emilio: Is this declaration index thing local to each element you compute?
<fantasai> TabAtkins: There's a 2nd thing you can provide, which is a keyword indicating whether this value should be shared by all elements with this style, or be element-specific.
<fantasai> TabAtkins: Then it either includes the element identity in the caching key or not
<weinig> q-
<fantasai> emilio: Say you have 2 selectors with random() and you have an element that matches one, and another that matches both of them
<fantasai> emilio: so in one the index ...
<fantasai> TabAtkins: index is just the number of random() instances in that declaration.
<fantasai> TabAtkins: so for 'width' always index of zero
<fantasai> emilio: So if they're in different elements, they would be shared?
<fantasai> TabAtkins: if not adding extra "match-across-elements" keyword, then they all include element ident
<fantasai> TabAtkins: This is new, because CSS generally doesn't care where a value came from
<fantasai> TabAtkins: whether spell out in comma-separated selector list, or in a style attr, these are all the same
<fantasai> TabAtkins: I want to maintain that equivalence as much as possible
<fantasai> TabAtkins: so only including information from declaration it lives in. Nothing from style rule or selector.
<fantasai> emilio: So maybe some things that you would expect to work don't?
<fantasai> emilio: e.g. 10 elements, each with style blah: random()
<fantasai> emilio: ok, sounds good
<fantasai> TabAtkins: To be clear, having it vary by element is the default. You have to specify keyword to make it shared across elements.
<fantasai> TabAtkins: so common case would be what you expect there
<fantasai> emilio: seems reasonable
<fantasai> emilio: Shorthands maybe funny?
<fantasai> TabAtkins: it uses the declared property, e.g. in 'margin' you'd use 'margin' as part of the key, not the longhand names
<fantasai> TabAtkins: if you write 'margin: random(...)' you get equal margins on all four sides
<fantasai> TabAtkins: there's a bit of text in the draft about how this works for custom properties, it's a bit weird (unfortunately)
<fantasai> TabAtkins: unregistered vs registered properties, since the latter compute and the former don't before substitution
<fantasai> emilio: Might be confusing, but as long as we clarify impact of registration should be ok
<fantasai> astearns: over time
<astearns> ack fantasai
<emilio> fantasai: what we're trying to do is by default you get max randomness (varies by element, property, declaration index), but doesn't by where you declare it
<emilio> ... within an element you can make it shared by using the custom property
<emilio> ... can share across elements with the keyword tbd
<emilio> ... I think it's the right direction
<emilio> astearns: a bit concerned about oriol's comments
<fantasai> astearns: Concerned about Oriol not being convinced yet.
<emilio> TabAtkins: I feel strongly about this model
<emilio> fantasai: we should give oriol a chance to comment on here
<emilio> astearns: let's defer this to next week

@kbabbitt
Copy link
Collaborator

During the discussion I was mulling over how the caching rules would interact with animations. I guess as written, if random() is used on a property in a keyframe, the caching rules would take effect per-keyframe in the context of the element the animation is applied to, and that seems like it would result in sensible behaviors.

But it made me wonder if there's an additional dimension authors might want: per-iteration randomness. For example, I might implement a randomly-bouncing-around animation like this:

@keyframes bounce {
   0% { left: 0px; top: random(match-element, 0px, 1000px); }
  25% { left: random(match-element, 0px, 1000px); top: 1000px; }
  50% { left: 1000px; top: random(match-element, 0px, 1000px); }
  75% { left: random(match-element, 0px, 1000px); top: 0px; }
}

Where I want lots of elements to follow the same randomized path, maybe with different timing offsets so they "follow" each other, but I also want different random values drawn each time the animation cycles.

@mirisuzanne
Copy link
Contributor

As an author, I prefer more randomness as the default, with options to opt-into shared values - so I really like the change from per-element to match-element (or similar). Most of my use-cases would involve applying randomness to a list of items, and ideally getting different values per item. With the original proposal, I would be opting into the per-element behavior most of the time.

Without referencing spec lingo, I don't see match-element as a confusing name - I would never think of it as matching the unique element identity – but something like element-shared does seem even more clear.

I also love @kbabbitt's per-iteration use-case. I would use per-iteration behavior with or without element sharing. I'm not sure I've entirely tracked if either of those is already possible in the proposal.

@nt1m
Copy link
Member

nt1m commented Mar 19, 2025

@kbabbitt With the current proposal you should be able to use the custom ident for that:

@keyframes bounce {
   0% { left: 0px; top: random(match-element --iteration-1, 0px, 1000px); }
  25% { left: random(match-element --iteration-2, 0px, 1000px); top: 1000px; }
  50% { left: 1000px; top: random(match-element --iteration-3, 0px, 1000px); }
  75% { left: random(match-element --iteration-4, 0px, 1000px); top: 0px; }
}

@vmpstr
Copy link
Member

vmpstr commented Mar 19, 2025

Sorry if this was discussed already, but I was wondering about a case where one toggles some class with randomness on an element

.random {
 width: random(100px, 200px);
}
...
setInterval(() => target.classList.toggle("random"), 1000);

It sounded like this would only pick one random value and then persist that for all other uses of the .random class on that particular element, which doesn't sound too random.

I wonder if there is an opportunity to include some sort of a generation id in the cache key that updates if the style applicability is interrupted as in the case above. There is somewhat tangential precedent to this in contain-intrinsic-size's last remembered size and how that sticks around while the property is continually applied and is forgotten otherwise

@kbabbitt
Copy link
Collaborator

@kbabbitt With the current proposal you should be able to use the custom ident for that:

@keyframes bounce {
   0% { left: 0px; top: random(match-element --iteration-1, 0px, 1000px); }
  25% { left: random(match-element --iteration-2, 0px, 1000px); top: 1000px; }
  50% { left: 1000px; top: random(match-element --iteration-3, 0px, 1000px); }
  75% { left: random(match-element --iteration-4, 0px, 1000px); top: 0px; }
}

@nt1m as I understood the proposal, that would select a random value once for each keyframe, and then reuse those same values for every iteration of the animation. Is that not the case?

The use case I suggested called for a new random value, for every keyframe, for every iteration, but each time a random value is drawn, all elements to which the animation is applied get the same value.

@Loirooriol
Copy link
Contributor

we should give oriol a chance to comment on here

So after a week I'm getting more used to the new proposal. Not opposed, but still a bit concerned it may the a source of confusion if authors assume that random(--foo, 0, 1) will be like list-style-type: --foo or animation-name: --foo where different elements referencing the same ident get the same behavior.

@mirisuzanne
Copy link
Contributor

@Loirooriol I agree that might be something authors assume at first, but I'm not sure that's enough reason to make it the default? If the keyword we land on is clear enough (reading back, I like global) I think it becomes very understandable as a system, even if it's a different system from some custom-idents in CSS.

@tabatkins
Copy link
Member

Re: per iteration randomness, I agree that using the custom-ident option is the right answer here. If we tried to randomize per keyframe, as in Kevin's example, then it brings in the same issues/confusion that randomizing based on style rule would. Is there a difference between 0% { width: random(...): }, 50% { width: random(...): } and 0%, 50% { width: random(...); }? Both "yes" and "no" are confusing. Relying on authors to solve this themselves with a random ident makes the answer clear.

That said, if it is something we do end up wanting to support more automatically, I think the right answer is that the auto-generated key would, in addition to the property/index, incorporate the keyframes name and keyframe index. That would mean that each instance of random() in Kevin's example would be distinct by default.


I was wondering about a case where one toggles some class with randomness on an element [example toggling a class on and off]

Correct, that would be the same random value every time the class applied.

I'm pretty opposed to a "generation key" for this, as it would make things distinct in unexpected cases, like .foo { width: random(match-element, 100px, 200px); } when applied to elements that can be streamed in - if two elements appears in distinct styling passes they'd get distinct random values rather than sharing. It would also mean, I expect, that an element would change all its random values if you toggled display:none on and off.

If you're using scripting and want random values, you can always supply them yourself, at exactly the sharing granularity you desire.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-values-5] Maybe min, max and step should not be part of the random-caching-key, and agreed to the following:

  • RESOLVED: Accept the proposal for changing random caching as stated in the issue.
  • RESOLVED: Go with element-shared for now, keep renaming issue open because many people are unhappy with it
The full IRC log of that discussion <dholbert> ScribeNick+ dholbert
<fantasai> scribe+
<fantasai> TabAtkins: The random() functions currently, by default, are maximally random.
<fantasai> TabAtkins: two axes controlling how they resolve
<fantasai> TabAtkins: 1. Which property they're in, and what index in the declaration
<fantasai> TabAtkins: 2. whether same across all elements or different
<fantasai> TabAtkins: can change either of these
<fantasai> TabAtkins: Force same across properties/instances by applying custom ident
<fantasai> TabAtkins: or force same across elements by using special keyword
<fantasai> TabAtkins: some opposition from oriol about the way that the element sharing was phrased
<fantasai> TabAtkins: He preferred previous model where elements shared by default, and opt out
<fantasai> TabAtkins: Since then several people commented preferring that starting with maximal randomnes is better
<oriol> q+
<astearns> ack oriol
<fantasai> TabAtkins: so my proposal is to adopt the proposed model, and then address naming quesiton
<fantasai> oriol: My concern is that when you provide the keyword, not clear that you would share across instances but not across elements
<fantasai> oriol: In our other properties with custom elements, you get same behavior for same custom ident
<fantasai> oriol: I worry it could create some confusion
<fantasai> oriol: But I think if you provide random() with no parameter, maximizing randomness is better
<astearns> ack fantasai
<TabAtkins> fantasai: one option is that "no parameters" is maximum randomness, using a custom ident gives you global correspondence, and using custom-ident+a keyword gives you shared in an element, but differetn across elements
<fantasai> TabAtkins: Yes, that would be a "per-element" keyword, discussed already
<fantasai> TabAtkins: I don't like that because it breaks independence of omitted chunks
<fantasai> TabAtkins: omitting all params shouldn't be different from the behavior of omitting each individually
<fantasai> TabAtkins: We usually adhere to that model, so I prefer to stick to that model.
<fantasai> TabAtkins: Specifying peels back specific aspect of randomness.
<fantasai> TabAtkins: rather than doing an inversion as soon as you specify anything
<fantasai> fantasai: I don't mind either way, just saying this is another possible syntax model.
<fantasai> astearns: so should we resolve on this model of maximal randomness?
<fantasai> RESOLVED: Accept the proposal for changing random caching as stated in the issue.
<fantasai> TabAtkins: We need to name the share-randomness-across-elements keyword
<fantasai> TabAtkins: some options were `element-shared` and `all-elements` ... other suggestions in issue
<fantasai> Proposal is at https://github.com//issues/11742#issuecomment-2707697957
<fantasai> [this should go into the resolution]
<fantasai> astearns: Do we have existing keywords related to this idea?
<bramus> q+
<fantasai> Suggested keywords https://github.com//issues/11742#issuecomment-2708387121
<astearns> ack bramus
<astearns> ack fantasai
<bkardell_> +1
<TabAtkins> fantasai: I think I agree with tim on match-element being confusing, it doesn't really convey... we're not matching this value to this element, liek it does in VT
<TabAtkins> fantasai: it's "this function should match across all elements", different concept
<bkardell_> thisfunctionshouldmatchacrossallelements
<TabAtkins> astearns: should we resolve on 'element-shared'?
<TabAtkins> fantasai: element-shared sounds weird
<bkardell_> I think I disagree all elements sounds more obv
<TabAtkins> straw poll: 1) element-shared, 2) all-elements, 3) both of these suck, i have a better idea
<TabAtkins> 1
<schenney> 1
<ydaniv> just "shared"?
<astearns> 1
<bkardell_> 1
<Kurt> 1
<fantasai> 2, but I wish I could pick 3
<miriam> 2
<alisonmaher> 1
<bramus> 1
<ydaniv> 1
<oriol> Maybe 3) shared-across-elements? Or 2)
<vmpstr> (abstain)
<kizu> 2
<dbaron> 1 (weakly)
<dbaron> (what about across-elements?)
<fantasai> +1 dbaron
<oriol> Not clear if across-elements is varying across elements or caching across elements
<kbabbitt> match-across-elements ?
<fantasai> fantasai: match-elements ?
<fantasai> TabAtkins: too close to v-t-n: match-element
<fantasai> ydaniv: by-property?
<dholbert> (I'm not particularly enthusiastic about either of the options but I don't have a better suggestion )
<emilio> (same as dholbert)
<fantasai> PROPOSED: Go with element-shared for now, keep renaming issue open because we're all unhappy with it
<fantasai> RESOLVED: Go with element-shared for now, keep renaming issue open because many people are unhappy with it

@romainmenke
Copy link
Member

I also find match-element to be very confusing.

I read it as something that causes the random base value to be specific or matched to one unique element.

Maybe because match makes me think about identity and element is singular?

Is it clearer by using a plural? e.g. match-elements

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