Skip to content

[css-gaps-1] Define interpolation behavior #12431

Closed

Description

@kbabbitt

Because the width, style, and color properties can now take lists of values, including lists with repeaters, we need to define what the interpolation behavior for these more complex values might look like.

Activity

  1. flackr commented on Jul 11, 2025

    @flackr
    Contributor

    FYI the repeat syntax is specified in https://www.w3.org/TR/css-gaps-1/#lists-repeat

    I think that non-auto repeaters should be able to combine with non-repeater values and other repeaters intelligently, e.g.

    Interpolating repeat(3, red) to repeat(2, red) blue should interpolate at 50% to repeat(2, red) #770077.

    In general this probably means intersecting the range of the repeater and pulling out any values which are no longer part of it.

    Similarly, we should be able to have intelligent repeater list extension when you don't have auto values. When there are auto values we should create additional values before/after the auto to match the other value's preceding / following non-auto lengths, e.g.

    Interpolating green repeat(auto, red) green to repeat(auto, blue) green should interpolate at 50% to #007777 repeat(auto, #770077) green.

  2. kbabbitt commented on Jul 15, 2025

    @kbabbitt
    CollaboratorAuthor

    In cases where there are no auto-repeaters, I think we can indeed be smarter.

    I think it gets tricky when there are auto-repeaters that don't line up with each other, because then the gap-by-gap interpolations can depend on the total number of gaps, which we don't know at computed-value time. For example, if we have:

    from { column-rule-width: 1px repeat(auto, 2px) 3px 4px 5px; }
    to   { column-rule-width: 6px 7px repeat(auto, 8px 9px) 10px; }

    The pairs of values will line up as follows:

    4 gaps:
    1px 3px 4px  5px
    6px 7px 8px 10px
    
    5 gaps:
    1px 2px 3px 4px  5px
    6px 7px 8px 9px 10px
    
    6 gaps:
    1px 2px 2px 3px 4px  5px
    6px 7px 8px 9px 8px 10px
    
    7 gaps:
    1px 2px 2px 2px 3px 4px  5px
    6px 7px 8px 9px 8px 9px 10px
    

    I can't figure out how to generalize that expansion without it being a function of the number of gaps. The values that pair with 3px and 4px would need to be pulled out of the end of the region covered by the auto repeater. But without knowing the number of gaps, how do you know where in the auto repeater to start pulling values from?

  3. flackr commented on Jul 15, 2025

    @flackr
    Contributor

    Oh yeah, my idea doesn't work if there are multiple values in the auto repeater, since as you point out we can't know which values to pull out at the right side of the auto repeater. However, for single value auto repeaters I think it works.

    There is lots of precedent in css for trying to coerce values to be interpolable when possible - it may be worth auto-aligning single-auto-value repeaters and the left-hand side of multi-value auto repeaters even if we can't extend the right side of multi-value auto repeaters.

  4. flackr commented on Jul 15, 2025

    @flackr
    Contributor

    The other option would be some kind of mix function that doesn't resolve the actual values in the computed style, e.g. produces a value like mix(first-keyframe, second-keyframe, percentage), however I worry this would add a lot of complexity to specify and implement.

  5. kbabbitt commented on Jul 15, 2025

    @kbabbitt
    CollaboratorAuthor

    I guess interpolate() might fit that purpose at the spec level, but I agree that implementing it would add a lot of complexity for what I would guess is a rare-to-nonexistent use case.

  6. jav099 commented on Jul 21, 2025

    @jav099
    Collaborator

    We're trying to come up with generalized definitions for the interpolation behavior, whilst keeping repeaters in mind. This is a summary of what we have been discussing so far and what we'd like other people's feedback on.

    Repeat syntax is defined here: https://www.w3.org/TR/css-gaps-1/#lists-repeat

    Current implementation (Chromium).

    For now we are interpolating by computed value, meaning that list lengths must be of same size.
    Moreover, we are only interpolating repeaters with other repeaters that have same repeat_count and len(repeated_values) (where we have repeat(repeat_count, [repeated_values]).
    This is the same thing that grid is doing for template-rows.

    So for example, we will interpolate these:
    5px → 15px
    5px 10px → 20px 30px
    repeat(2, 10px 20px) → repeat(2, 15px 30px)
    10px repeat(2, 3px) → 30px repeat(2, 10px)

    But on the other hand we will not interpolate these:
    5px → 10px 15px
    repeat(2, 10px 20px) → repeat(2, 15px)
    10px repeat(4, 3px) → 30px repeat(2, 10px)

    However, for Gap Decorations, we believe that it makes sense to expand this behavior and allow other combinations of values to interpolate.

    LCM to allow for different length lists

    Firstly, one easy improvement is that we could interpolate lists of different lengths by doing repeatable list interpolation in which

    if the two lists have differing numbers of items, they are first repeated to the least common multiple number of items (LCM). Each item is then combined by computed value. If a pair of values cannot be combined or if any component value uses discrete animation, then the property values combine as discrete.

    background-size interpolates this way.

    An example of this is included for clarity:

    1. Interpolating 50px 100px → 20px 30px 40px.
    2. LCM(2,3) = 6
    3. becomes [50px,100px,50px,100px,50px,100px] → [20px, 30px, 40px, 20px, 30px, 40px]
    4. (50→20), (100→30), (50→40), (100→20), (50→30), (100→40)

    Handling Repeaters

    However, the tricky part comes when we include repeaters, so we're looking to define how repeaters would behave when interpolating with other repeaters as well as other non-repeaters.

    @flackr mentioned above a concrete example of something we might want to do:

    repeat(3, red) to repeat(2, red) blue should interpolate at 50% to repeat(2, red) #770077

    I will change this example to use Widths since that's easier to reason about:

    repeat(3, 10px) → repeat(2, 10px) 20px would become at 50% repeat(2, 10px) 15px.

    This implies that we're doing some logic where we find the intersection from these repeaters, see how they line up, and pull out any values, which leads to the rather clean result of repeat(2, 10px) 15px.

    On the flip side, this seems to be somewhat at odds of the LCM Repeatable List interpolation behavior. There will be some cases where this will not be possible, or at least I can't quickly see how we could generalize that into also interpolating something like: repeat(3, 10px 15px) → repeat(2, 10px) 20px, or something even more complicated like r(3, 5px 10px 15px 20px) → r(4, 15px 20px 25px)

    Potential Option 1: LCM on the raw lists

    One potential solution would be to treat repeaters as exactly what they are, lists of values, and interpolate using LCM as such, "creating" (I put quotes on "creating" because we wouldn't have to actually create them, using modulo would be sufficient I believe) lists of equal length and then interpolating matching pair indices.

    So for example, r(2, 10px) → r(2, 15px 20px)

    1. we interpolate as 10px 10px → 15px 20px 15px 20px
    2. We expand each list to the LCM length: 10px 10px 10px 10px → 15px 20px 15px 20px
    3. We interpolate matching indices.

    This would also solve the problem of interpolating between repeaters and non-repeaters.

    For example, r(2, 10px 15px) → 20px:

    1. Apply LCM.
    2. We interpolate as 10px 15px 10px 15px → 20px 20px 20px 20px

    To go back to the previous example that @flackr illustrated (but with widths) repeat(3, 10px) → repeat(2, 10px) 20px.

    The first thing to consider would be how we deal with the fact that the lengths of the to and from don't match (2 vs 1), at least if we take r(...) as one element.

    Element-wise LCM

    Another option would be to apply LCM element-wise:

    1. So for repeat(3, 10px) → repeat(2, 10px) 20px
    2. We would first have r(3, 10px) r(3, 10px) → r(2, 10px) 20px.
    3. Then we interpolate pairs r(3, 10px) → r(2, 10px) and r(3, 10px) → 20px

    Even though this is intuitive and easy to generalize, we end up interpolating potentially fairly large number of elements (9 pairs in this small example).

    If this is not a path we want to go down to, I could see a potential work around as treating the length of repeat(3, 10px) as 3 and repeat(2, 10px) as 2), which leads to this example then interpolating between matching list lengths. However, I suspect this would lead us to have to expand the repeaters in the general case, to be able to do pairwise interpolation, but there might also be a way around this.

    The intermediate values

    Another question that arises, is what do we expose as the computed value at x% through the animation?

    All of the options described previously would entail "losing" the repeat syntax for the in-between steps of the interpolation.

    So for example something like r(2, 10px) → r(2, 15px 20px):

    1. we interpolate as 10px 10px → 15px 20px 15px 20px
    2. We expand each list to the LCM length: 10px 10px 10px 10px → 15px 20px 15px 20px
      a) at 50% this would be something like: 12.5px 15px 12.5px 15px
      b) although in this particular case it is easy for us to derive a repeater from this, it is not a trivial task in the general case. @oSamDavis was able to come up with a potential formula that could allow us to derive a repeater from a list in the general case, but we haven't rigorously tested it so might not work for all scenarios.
      c) As a result, at intermediate keyframes we will return only the expanded list of values (no repeat()), e.g. [12.5px, 15px, …], which may or may not be desired.
    3. We interpolate matching indices.

    My personal take:

    I would lean towards the LCM-based, “repeatable list” interpolation model. It’s simple, and it ensures that every index in both lists has a clear pair to interpolate against. That predictability is a huge win, especially when authors rely on computed values at arbitrary keyframes.

    Flattening out to an expanded array every frame makes the math easy, but it sacrifices author-friendly semantics if someone inspects the computed style mid-animation. Personally, I’d be comfortable with the trade-off, but maybe we could also consider a post-processing pass at the end of interpolation (or at discrete checkpoints) to re-collapse patterns that trivially map back into a repeat() form.

    Regarding performance, LCM could balloon the list lengths dramatically. In practice, gaps tend to be small lists, so it’d probably never become too big, but we already handle potential cases such as this in Chromium https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/animation/list_interpolation_functions.cc;drc=3dc8b5b36eaa1d68f5caa5e36444fcb3e204d460;l=108

    Would appreciate more people's thoughts and takes on how we could solve these issues and come up with behavior that is generalizable among lists of different types (repeaters and non-repeaters) and different lengths.

  7. css-meeting-bot commented on Aug 6, 2025

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-gaps-1] Define interpolation behavior, and agreed to the following:

    • RESOLVED: define gap decoration animations using LCM methods
    The full IRC log of that discussion <kbabbitt> javierct: for interpolation, part of what we're trying to achieve is to define what interpolation behavor will be
    <kbabbitt> ... currently the way we've been goingt about it is how grid-template-rows/columns does it
    <kbabbitt> ... specifically we're concerned about repeater part
    <kbabbitt> ... g-t-r/c will only interpolate repeaters if list lengths match exactly
    <kbabbitt> ... that's how we started going about it, then there was some discussion
    <kbabbitt> ... makes sense for grid to do that but not necessarily gap decorations
    <kbabbitt> ... might want to interpolate repeaters with different counts
    <kbabbitt> ... been some discussion about this, some ideas that have been floated
    <kbabbitt> ... ideas about lining up repeaters, pullinhg out certain values so things line up
    <kbabbitt> ... I thought about it multiple ways, couldn't find a way to generalize that behavior
    <kbabbitt> ... I put my notes in the issue, but my conclusion / suggestion was that maybe the best way is to lean towards treating repeaters as list of values
    <kbabbitt> ... expanding it
    <kbabbitt> ... and then as a second step, use LCM based method of repeatable list interpolation we have for other properties
    <kbabbitt> ... match lengths of resulting lists that way
    <kbabbitt> ... question would arise, as we mentioned before, what we'd lose before with this way of doing it is that, intermediate values wouldn't necessarily reconstruct repeater
    <kbabbitt> ... if the author interpolates a repeater with another repeater, intermediate steps would get list of values rather than repeater
    <kbabbitt> ... might be a way to reconstruct repeater
    <kbabbitt> ... from these intermediate lists of values
    <kbabbitt> ... but I haven't been able to generalize this
    <kbabbitt> ... even if one exists, maybe it would be easier for an author if it's not a lot of math to
    <kbabbitt> ... if they inspect an arbitrary keyframe, it's easier to think about what these lists of values should be
    <kbabbitt> ... but if we have a complicated formula to reconstruct a repeater, authors might not expect that
    <kbabbitt> astearns: it's fine to have intermediate values be explicit lists
    <kbabbitt> ... main thing is intermediate values need to be representable in a way the property can accept
    <kbabbitt> ... if we're fudging the list of values in order to recreate a repeater, that makes it harder for script to process
    <kbabbitt> javierct: agree with that
    <kbabbitt> ... lean towards this idea of expanding lists
    <kbabbitt> ... even if they don't line up in repeat count, match up lists with lcm method
    <kbabbitt> ... that would solve interpolation between repeaters of different counts
    <kbabbitt> ... but there's the question of auto repeaters
    <kbabbitt> ... we don't know the number of gaps at computed-value time
    <kbabbitt> ... so we can't expand those lists
    <kbabbitt> ... if we see an auto repeater, would we only be able to match it with another auto repeater?
    <kbabbitt> ... and if not, don't interpolate?
    <kbabbitt> ... that would be the trickier part
    <kbabbitt> ... my gut would be, lcm method solves all cases except this one, maybe we need to find some sort of exception
    <kbabbitt> ... treat auto repeaters as their own element inthe list and only interpolate between auto repeaters if the list lengths are the same
    <kbabbitt> astearns: you mentioned we don't know how many gaps there are
    <kbabbitt> ... but do we need to?
    <kbabbitt> ... could the rule be to expand auto repeaters to point where there's a common multiple between them?
    <kbabbitt> javierct: so repeat(auto, 10 5) and repeat(auto, 2 10)
    <kbabbitt> ... expand auto into into 10, 5
    <kbabbitt> astearns: Haven't worked through an example in my mind
    <kbabbitt> ... Would expect that interpolation shouldn't have anything to do with how many or which values there are
    <kbabbitt> javierct: i can see that
    <kbabbitt> ... can't think of any issues that would come up with that
    <astearns> s/values there are/values are actually used in a given layout/
    <kbabbitt> +1, I think we can lcm() between auto repeaters
    <kbabbitt> astearns: want to go back to motivation
    <kbabbitt> ... solution makes some sense but we passed over the 'why'
    <kbabbitt> javierct: we don't have the interpolation behavior for gap decorations defined
    <kbabbitt> astearns: definition could be as simple as flip at 50%
    <kbabbitt> ... would an author want to interpolate between different gap decorations?
    <kbabbitt> q+
    <kbabbitt> javierct: want to change color going smoothly from one to another, smaller to bigger
    <kbabbitt> ... I think smooth works correctly for these rather than discrete
    <kbabbitt> ... ability we want to give the author
    <kbabbitt> ... as far as I'm aware, with colors, that's what we usually do
    <kbabbitt> ... at the very least match it, because that is what we do when interpolating colors
    <kbabbitt> ... border does it this way
    <kbabbitt> ... want to match author expectation
    <kbabbitt> astearns: makes sense
    <astearns> ack kbabbitt
    <Kurt> kbabbitt: at minumum, the existing column-rule width/style are interpretable, we need to keep for back-compat
    <Kurt> ...the question is what we do for lists now, LCM makes sense for non-auto repeater, it works...
    <Kurt> ...for auto repeater to auto repeater, we can take the two and LCM the combination of those, I can't see why that wouldn't work
    <Kurt> Javier: can you see a case for going from non-auto to auto, or auto to a list of values
    <Kurt> kbabbitt: because of how auto repeaters work, they take up the slack, so it would be an auto repeater to another auto repeater with a list length of 0, and i don't know how that would work
    <Kurt> astearns: maybe it makes sense to resolve to define interpolation to use the LCM method, and define new issues later
    <Kurt> ...any objections?
    <Kurt> RESOLVED: define gap decoration animations using LCM methods
  8. flackr commented on Aug 6, 2025

    @flackr
    Contributor

    Apologies I didn't realize there was a breakout for this. I don't quite understand what was resolved. @kbabbitt @jav099 , is the idea that we use LCM extension to match list lengths but then fail to interpolate if the paired values aren't compatible (e.g repeater to non-repeater or repeaters with different args)?

  9. jav099 commented on Aug 7, 2025

    @jav099
    Collaborator

    @flackr not exactly, we will interpolate between different types in most cases. You can see updated proposal in the linked PR

    #12583

  10. 7 remaining items

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions