Skip to content

[css-cascade] Additive CSS #1594

Open
Open

Description

@birtles

Additive CSS

The following material has been prepared for discussion at the August 2017 CSS F2F in Paris, France.

Outline

The goal of this discussion is to:

  1. Explain additive animation and why it was added to Web Animations
  2. Propose a general-purpose approach to combining CSS property values that is useful in static contexts too
  3. Decide if it is suitable for to ship additive animation in Web Animations and CSS Animations 2 knowing that an overlapping general-purpose feature may arrive in the near future

Part 1: Additive animation

This section describes what additive animation is, how it works, and why it is part of Web Animations.

What is additive animation?

Typically when two independent animations target the same property on the same element, one animation will clobber the other.

This can be either because of the nature of the CSS cascade, e.g.

.spin {
  animation: spin 2s infinite;
}
.swell {
  animation: swell 1s alternate 2;
}
@keyframes spin  { to { transform: rotate(1turn) } }
@keyframes swell { to { transform: scale(2) } }

(Demo)

Or simply because only one animation is ever applied to a given property, e.g.

div {
  animation: spin 2s infinite, swell 1s alternate 2;
}
@keyframes spin  {
  from { transform: rotate(0turn) }
  to { transform: rotate(1turn) }
}
@keyframes swell {
  from { transform: scale(1) }
  to { transform: scale(2) }
}

(Demo)

Additive animation, however, allows the two independent animations to be combined together.

For example, using the Web Animations API we can express the above as:

elem.animate({ transform: 'rotate(1turn)' },
             { duration: 2000, iterations: Infinity });
elem.animate({ transform: 'scale(2)', composite: 'add' },
             { duration: 1000, iterations: 2, direction: 'alternate' });

(Demo - requires Firefox Nightly or Chrome Canary)

CSS Animations 2 proposes to expose this using the animation-composite property.

e.g.

div {
  animation: spin 2s infinite, swell 1s alternate 2;
}
@keyframes spin  { to { transform: rotate(1turn) } }
@keyframes swell { to { transform: scale(2); animation-composite: add } }

Note that this only solves the second of the two problem cases above. To solve the case where the CSS cascade results in only one animation appearing in the computed value of animation-name we need the more general additive approach described in part 2 of this proposal.

How does it work?

Firstly, animations that target the same property are arranged in order. This order is important because:

  • for some types of properties, the addition operation might not be commutative (e.g. addition of transform lists involves concatenating lists which is equivalent to matrix multiplication which is not commutative), and
  • if animation A is additive, but animation B is not, what should be the result? The answer is it depends on which is higher in the stack.

Web animations defines this as an effect stack. For animations created using the API the order is roughly equal to the order in which the animations are generated but for CSS animations, it is based on the order of the animations listed in the animation-name property.

At the bottom of the stack is the unanimated value, called the underlying value. It is possible for a single animation to be combined with the unanimated value using this same mechanism, e.g.

elem.style.transform = 'translate(100px)';
// The following animation represents an offset from the initial translation
elem.animate({ transform: 'translate(100px, 100px)', composite: 'add' },
             { duration: 1000, iterations: 2, direction: 'alternate' });

(Demo - requires Firefox Nightly or Chrome Canary)

The specific procedures for adding each property needs to be defined just as we define the specific procedures for interpolating each property. The intention is that both the interpolation procedures and addition procedures for common types will be defined in CSS Values and Units and then specifications
that define new property types can defined further procedures.

One technical detail is that in some cases there are multiple notions of addition. For example, for a filter list we can define blur(2px) + blur(5px) as either blur(2px) blur(5px) or blur(7px).

As it turns out SVG uses both these modes. When two independent animations are added together it does list concatenation, i.e. blur(2px) blur(5px) from above. When an animation is defined to build on itself on each iteration it adds the function components, i.e. blur(7px) from above.

(Technically SVG only makes this distinction for transform animations and it doesn't actually do list concatenation, but matrix post-multiplication which is functionally equivalent.)

Web Animations calls these two modes add and accumulate respectively, and collectively calls these composite operations.

Why does Web Animations have additive animation?

The original brief for Web Animations was that it should represent the common base for CSS Animations, CSS Transitions, and SMIL/SVG animation. SMIL/SVG animation has additive animation so therefore Web Animations has additive animation.

Is it needed?

The most common usage of additive animation is for combining transform animations. A number of these use cases can be addressed using the individual transform properties from CSS Transforms 2. However, this does not cover all use cases such as:

  • animating translateX and translateY independently (as required by libraries such as GreenSock Animation)
  • animating the same transform component independently (e.g. the translate example above)
  • animating other properties such as filter (e.g. animating the blur on an element that already has a sepia effect applied), or even opacity (e.g. animating the opacity to +50% of its current value).

The other alternative to additive animation is to simply add more and more <div> elements to represent each possible independently timed animation component. This approach does not scale well to dynamic animations where a potentially unbounded number of animations might be generated dynamically and having to adding elements for this is, in this author's opinion, one of the more frustrating parts of authoring CSS at the moment.

Part 2: Static CSS and addition

As it turns out, the notion of combining CSS property values is useful beyond just animations.

Use cases

e.g. truly generic classes

.nostalgic {
  filter: sepia(60%) !add;
}
.blurry {
  filter: blur(5px) !add;
}
<div class="nostalgic"></div>
<div class="blurry"></div>
<div class="nostalgic blurry"></div> <!-- No problem -->

e.g. modifying underlying values

.translucent {
  opacity: -50% !add;
}

e.g. combining animations defined by different rules that target the same property

.spin {
  animation: spin 2s infinite !add;
  animation-composite: add;
}
.swell {
  animation: swell 1s alternate 2 !add;
  animation-composite: add;
}

e.g. overriding certain list items by using the cascade order

button {
  filter: blur(2px);
}
button:focus {
  /* This will override the blur() item but not the grayscale() due to cascade order */
  filter: blur(0px);
}
button:disabled {
  filter: grayscale(50%) !add;
}

Proposal: !add annotation

I believe both David Baron and Tab Atkins have at some point considered the idea of an !add annotation for this.

For static CSS the CSS cascade (that is selector specificity etc.) provides the ordering for composition. The absence of an !add annotation on a declaration indicates that any lower priority declarations are ignored as they are today (even if those lower priority declarations have !add annotations).

Issues that need consideration

Some issues that would need to be considered as part of such a proposal:

  • Is !add enough? Do we need both the add and accumulate modes described above?
  • Are there other modes we need?
    • e.g. multiplication to produce 75% of the current opacity value. Or should that be achieved by extending calc() to be able to work with the underlying value?
  • Can we relax the range checking used when an !add annotation is in place so that is is possible to write opacity: -50% !add?
    (We already do this internally for SMIL and I suppose we want to do this for animation-composite anyway.)

Part 3: Shipping additive animations

The purpose of raising this proposal is to consider the implications of shipping additive animations now and later introducing a more generalized mechanism.

How would the additive features specified in part 1 change if we also eventually do part 2?

Example 1: Setting the composite mode in a single keyframe

// Current API (part 1)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px)', composite: 'add' ], 2000);

// Possible future API (part 2)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px) !add' ], 2000);

// What if both are combined? (part 1 & part 2)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px) !add',
                 opacity: 0.5,
                 composite: 'accumulate' ],
             2000);
// Presumably we would define a rule that says that if we have an !add
// annotation then that overrides any composite member such that in the above
// 'transform' uses the 'add' composite mode, and 'opacity' uses the
// 'accumulate' composite mode.
// As such, I think we can harmonize these two approaches.

Example 2: Setting the composite mode across all keyframes

// Current API (part 1)
elem.animate({ transform: [ 'translate(100px)', 'translate(200px)' ] },
             { composite: 'add', duration: 2000 });

// Possible future API (part 2)
elem.animate({ transform: [ 'translate(100px) !add',
                            'translate(200px) !add' ] },
             2000);

// What if both are combined? (part 1 & part 2)
elem.animate({ transform: [ 'translate(100px)', 'translate(200px) !add' ] },
             { composite: 'accumulate', duration: 2000 });
// Again, presumably we say that !add overrides any composite member such
// that the first value uses 'accumulate' and the second value uses 'add'.
// As such, there does not appear to be any conflict here.

I think these same principles could equally apply to the CSS syntax if we choose to ship that too. For example, taking Example 1 and translating it into the proposed CSS syntax from CSS Animations 2:

/* Current API (part 1) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px); }
  to   { transform: translate(200px); animation-composite: add; }
}

/* Possible future API (part 2) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px) }
  to   { transform: translate(200px) !add }
}

/* What if both are combined? (part 1 & part 2) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px) }
  to   { transform: translate(200px) !add;
         opacity: 0.5;
         animation-composite: accumulate }
}
/* As above, the 'transform' value would use 'add' while the 'opacity' value
   would use accumulate. */

It seems to me that there is a relatively straightforward path for harmonizing the approach in part 1 if we also do part 2 and that, in fact, the features introduced in part 1 might be useful in combination with part 2 as a way of bulk-setting the composite operation.

Activity

  1. FremyCompany commented on Jul 12, 2017

    @FremyCompany
    Contributor

    Still relevant on the topic:
    https://lists.w3.org/Archives/Public/www-style/2013Apr/0711.html

    I have some more thoughts that are not reflected in this 4 years old thread, most notably the role custom properties could have here to improve DRY, but I would happy to discuss this further at the F2F.

  2. jackdoyle commented on Jul 25, 2017

    @jackdoyle

    I wish I had more time to dig into this, but I wanted to chime in briefly...

    I fear that this feature won't really address the pain points that animators face in their day-to-day work. Here are 2 quick demos of where I think it might miss the mark:

    1. https://codepen.io/GreenSock/pen/37ab2315a5599df13a172edeeffc53ee?editors=0010
    2. https://codepen.io/GreenSock/pen/20ad5e2cc4c7f0e02e471d70927e73a6?editors=0010

    (Read the comments in the JS code)

    I'd LOVE to be able to tap into these technologies under the hood in GSAP, but from what I understand, it doesn't appear as though this feature will deliver what we really need in order to do that. Layering things on top of each other might help in some scenarios, but there are quite a few (as demonstrated above) that still seem to be impossible/cumbersome. Or perhaps I'm just missing something(?) Please forgive me if I botched something in the implementation.

  3. FremyCompany commented on Jul 25, 2017

    @FremyCompany
    Contributor

    @jackdoyle Thanks for the use cases, they seem relevant.

    Here's a similar thing written using the solution I proposed:

    :root {
      --GSAP: 1000;
      --GSAP_ROTATE: var(--GSAP) 0;
      --GSAP_SCALE: var(--GSAP) 1;
      --GSAP_TRANSLATE: var(--GSAP) 2;
    }
    #WebAnimation1 {
      transition: transform[] 1s linear;
    }
    #WebAnimation1.State1{
      transform[ GSAP_ROTATE 0 ]: rotate(180deg)
    }
    #WebAnimation1.State2{
      transform[ GSAP_TRANSLATE 0 ]: translateX(200px);
    }
    #WebAnimation1.State3{
      transform[ GSAP_ROTATE 1 ]: rotate(-180deg);
    }
    

    Now you can add classes State1, State2 and State3 in whichever order you want and get a deterministic transition. The way this works is that the subtransforms will always be sorted by number sequence, in descending order. That means "1000 2 0" (GSAP_TRANSLATE 0) will always be first, "1000 0 1" (GSAP_ROTATE 1) will be second and "1000 0 0" (GSAP_ROTATE 0) will always be third.


    You can override these values later in your stylesheet if needed by referencing their list-id:

    #WebAnimation1.State1b {
      transform[ GSAP_ROTATE 0 ]: rotate(270deg);
    }
    

    I also have a proposal to allow you to leave out the last number as a automatically-unique value (you cannot override it later like I did with State1 and State1b, but its relative ordering will be solved automatically using css cascade order)

    #WebAnimation2.State1{
      transform[ GSAP_TRANSLATE + ]: translateX(200px);
    }
    #WebAnimation2.State2{
      transform[ GSAP_TRANSLATE + ]: translateY(100px);
    }
    

    Last but not least, this gives a lot of flexibility when you need mixing frameworks: If there is another framework, you can (as an author) decide in which order the operations of these frameworks should take place. For instance, if this framework ("ANIMF") should take place after GSAP you can ensure it uses

    --ANIMF: 500
    

    and if you want to run its effects before GSAP

    --ANIMF: 1500
    

    but you can also have its effect run during GSAP Scale phase

    --ANIMF: var(--GSAP_SCALE) 1000
    

    Since variables cascade you can even decide that element per element by redefining GSAP and ANIMF per element.

    If you need some operations of ANIMF to happen before GSAP but others to happen after, you can use

    --ANIMF: 500;
    --ANIMF_TRANSLATE_EFFECTS: var(--GSAP_TRANSLATE) 1000;
    
  4. birtles commented on Jul 25, 2017

    @birtles
    ContributorAuthor

    @jackdoyle I'm pretty sure you can address that use case when we add the proposed composite order control.

  5. jackdoyle commented on Jul 31, 2017

    @jackdoyle

    Sorry about the late response. For some odd reason, I wasn't notified of your replies, nor am I allowed to subscribe to the thread (even now).

    @FremyCompany I can't mock up that solution in a codepen because those features aren't implemented in any browser yet, right? I read through your suggestion several times and I couldn't grasp how this would solve the problem I was pointing out. Likely because of my own cognitive deficiencies :)

    A demo would be super helpful (though I realize it's probably not possible today).

    @birtles I'm in a similar situation. Does the proposed composite order control allow folks to, for example, have 100 different elements flying around and randomly choosing various translateX/translateY/rotate/scale values with completely different timings without a bunch of overhead that tracks the order of previous transforms?

    For example, with this additive stuff, after animating to rotate(180deg) how can I get back to 0deg without tracking previous stuff? It seems I'd have to do rotate(-180deg) in order to cancel out the previous rotate(180deg), right?

    If there's a translateX(100px) and halfway through that, I need to animate translateY(200px) (without interfering with the translateX()) but previously there had already been a translateY(500px) applied, how can I ensure that it arrives at translateY(200px) instead of translateY(700px)? Would I have to getComputedStyle() and parse through the current values? What if it was part-way through a translateY(500px) animation (thus getComputedStyle() won't return the destination)?

    How much work will it be for the average developer to animate to absolute translateX/translateY/scaleX/scaleY/rotate values with completely offset timings from each other (overlaps)? Got a demo of how this will solve that problem? It'd be super cool if you could fork the demos I provided and show me what you mean. Please forgive my slowness.

  6. birtles commented on Aug 1, 2017

    @birtles
    ContributorAuthor

    Hopefully I've restored Jack's comment which I accidentally edited. Sorry about that. (And I find it quite odd I can edit someone else's comment!)

  7. birtles commented on Aug 1, 2017

    @birtles
    ContributorAuthor

    @birtles I'm in a similar situation. Does the proposed composite order control allow folks to, for example, have 100 different elements flying around and randomly choosing various translateX/translateY/rotate/scale values with completely different timings without a bunch of overhead that tracks the order of previous transforms?

    Yes. Obviously if they are different elements, then they have separate composite stacks so there should be no conflict. Within a single element, you can call getAnimations() and iterate over the returned Animations to see what animations it has running (and hence where in the stack you want to add new animations to get the desired result).

    For example, with this additive stuff, after animating to rotate(180deg) how can I get back to 0deg without tracking previous stuff? It seems I'd have to do rotate(-180deg) in order to cancel out the previous rotate(180deg), right?

    You can animate the rotate property to rotate(0deg) without specifying a from value to get the desired effect.

    If there's a translateX(100px) and halfway through that, I need to animate translateY(200px) (without interfering with the translateX()) but previously there had already been a translateY(500px) applied, how can I ensure that it arrives at translateY(200px) instead of translateY(700px)? Would I have to getComputedStyle() and parse through the current values? What if it was part-way through a translateY(500px) animation (thus getComputedStyle() won't return the destination)?

    Yes, if you're looking to animate the components independently in a pseudo-additive fashion like that despite the fact that they are expressed by the same CSS property, then, for now, you'll need to go through the result of getAnimations() to find your current target Y. You can annotate the Animation objects with extra metadata (e.g. the target X/Y values) when you generate them to avoid parsing the result of getKeyframes() later.

    There's some complexity but I think it's manageable and only present when all of the following are true:

    • You have a pseudo-additive animation, i.e. start point is not fixed and end point is and you have underlying animations on that component that you want to blend with the underlying animations
      • If start and end point are fixed you can cancel the existing animations on the necessary component
      • If start and end are additive, no extra work is necessary
      • If there are no underlying animations on the components, then obviously no extra work is necessary
    • And you are trying to animate components that are represented by the same property
      • For animating rotate etc. you can use the separate property

    If we find that this is common enough, then we can split off separate properties for the independent components that need to be animated in this way.

  8. AmeliaBR commented on Aug 2, 2017

    @AmeliaBR
    Contributor

    (I'm reading through the proposals & comments, so I'll be making a series of comments on separate points. Overall: I really want this feature in CSS!)

    As it turns out SVG uses both these modes. When two independent animations are added together it does list concatenation, i.e. blur(2px) blur(5px) from above. When an animation is defined to build on itself on each iteration it adds the function components, i.e. blur(7px) from above.

    (Technically SVG only makes this distinction for transform animations and it doesn't actually do list concatenation, but matrix post-multiplication which is functionally equivalent.)

    This is a very misleading summary.

    SVG/SMIL has two types of additive animations, yes: multiple independent animations adding together ("additive" animations) versus a single animation that increases its effect on each repeat ("accumulate" animations).

    Only certain data types are additive (number, length, angle, color, x-y pairs, and transformations). However, they normally behave identically as far as how the addition is calculated, regardless whether it is addition from separate animations or from repeats.

    The only time there is a difference is for <animateTransform>. And that has less to do with the non-commutative nature of transforms, and more to do with the structure of the <animateTransform>: the transform type (e.g., translate versus rotate) is defined separately from the values (parameters, i.e., the distance or the angle). Accumulation applies to the values, because it's a property of a single animation element, but addition applies to the complete effect.

    Example of accumulate and additive animations, for <animate> and <animateTransform>

    Presumably, if you used <animate> to animate a transformation by directly setting the transformation string as the value (as proposed for Level 2 SVG animations), then you would always be adding the complete animation, even for accumulations.


    Edited to add:

    SVG/SMIL also has an additive animation option that is so super-simple I forgot about it: the by attribute allows you to define a single-keyframe animation as a change relative to the base value on the element.

  9. AmeliaBR commented on Aug 2, 2017

    @AmeliaBR
    Contributor

    The most common usage of additive animation is for combining transform animations.

    Definitely the most common, but here are some others (remembering that we're not only talking about one animation adding to another, but also about animations adding to the base value):

    Unlike with transforms, creating an additive effect with nested <div> isn't even possible. (Well, except for maybe using percentages of container width or inherited font-size.) Even for transforms, having to add extra markup for every dimension of the transform is really hacky and ugly and annoying.

  10. AmeliaBR commented on Aug 2, 2017

    @AmeliaBR
    Contributor

    General thoughts on the !add proposal:

    This is one of the most straight-forward proposals I've seen for allowing partial or additive CSS declarations, which is a definite bonus. It fits nicely into CSS syntax (finally something else that uses !qualifier). And it directly builds on CSS cascading.

    The main thing that would need to be worked out is the addition rules for different data types. This would be similar to how we have interpolation rules for data types -- and with the similar understanding that some things can't be added and would be replaced instead.

    The simplicity could be a limitation when talking about more complex properties. For property values that consist of more than a single value, there usually wouldn't be a single universal way to define how to add two declarations.

    For some cases, this issue could probably be avoided if you could build up that complex value from custom properties, and then animate the custom property values in an additive way. This could be a solution to Brian's filter example. Normally, filters could add by adding an extra filter function at the end of the chain. But you could declare a filter: blur(var(--blur-radius)); and then make the --blur-radius declarations additive. (This of course assumes an easy way to declare the data types of custom properties for interpolation and addition. But that's going to happen anyway, right?)

    But there are other cases where you want to modify a complex value, which can't easily be deconstructed into custom properties. For example, to insert an image layer into a background-image stack. You need to add a comma token into your custom property value, and the syntax gets very fragile.

    For the specific issues Brian highlighted:

    Is !add enough? Do we need both the add and accumulate modes described above?

    I think accumulate is useful. But it doesn't fit into the !add syntax. Accumulation is a property of the animation repeat cycle, not of an individual declaration. The entire animation would either need to accumulate or not. And whether it accumulates or not would be separate from whether they underlying declarations were additive or not.

    (I'm assuming that, for !add in a @keyframes rule set, we are always adding on to the previous value from the cascade, and not on the previous keyframe. So it would be possible to have e.g. a 50% keyframe that was additive (so the first half of the animation is defined as a change from the base value), and then a 100% keyframe that wasn't additive (final value in the animation is the same regardless of the base value).)

    If you wanted to support accumulative repeats in addition to an !add syntax, I think the logical way to do so would be via a new animation-* property. Accumulation would be something like alternating repeats or a forwards-fill mode, that is part of the overall timing and repeat structure of the animation, not part of the keyframes.

    However, there would still need to be details to figure out:

    • How do you define the amount of change which becomes the amount to add? If we're being consistent with SVG/SMIL (and I think Web Animations), then the additive amount for each keyframe would be measured as the value for that keyframe relative to the from value. However, SVG/SMIL only does it this way if a from is set explicitly, or if the relative change is set explicitly with by. An animation defined with implicit from and explicit to does not accumulate in SVG/SMIL. Which doesn't translate nicely to a CSS model. Also, values and accumulate aren't well defined together in the spec; Firefox and Chrome treat them so that the values automatically become additive, then accumulative in their difference relative to the base value, instead of relative to the starting frame. Basically, it's a huge mess of logical inconsistencies and I'm not sure you'd want to automatically follow along to match.

    • How do accumulative animations interact with reverse-direction or alternating animations? SVG/SMIL does not have these. The definitions in Web Animations do not appear to discuss it.

    Are there other modes we need? e.g. multiplication to produce 75% of the current opacity value. Or should that be achieved by extending calc() to be able to work with the underlying value?

    Now we're getting into the complicated questions that the simple syntax can't cover. Which other combination options you want would depend on the specifics of each property type. The more complex the property, the more different possible ways you might have for combining values. Think of two text-shadow declarations: do you add them as a list, to create two shadows? Or do you add all the individual terms. So maybe there's a !concatenate list option versus !add? But how does this compare with !multiply for other properties? It quickly becomes a tangle.

    A simple syntax like !add needs a simple rule. Just like with the SVG/SMIL additive rules, that would probably mean a very restricted set of additive data types. As mentioned above, more complicated combinations would require CSS variables.

    Can we relax the range checking used when an !add annotation is in place so that is is possible to write opacity: -50% !add?
    (We already do this internally for SMIL and I suppose we want to do this for animation-composite anyway.)

    This would be very desirable. It should be similar to calc() and var() rules for range checking, where you only clamp when you can convert it to a meaningful final value.

    (This is probably it for me tonight. Sorry I didn't get a chance to review @FremyCompany's proposal. I'll be reading over the IRC minutes when I get online tomorrow.)

  11. birtles commented on Aug 2, 2017

    @birtles
    ContributorAuthor

    (I'm reading through the proposals & comments, so I'll be making a series of comments on separate points. Overall: I really want this feature in CSS!)

    As it turns out SVG uses both these modes. When two independent animations are added together it does list concatenation, i.e. blur(2px) blur(5px) from above. When an animation is defined to build on itself on each iteration it adds the function components, i.e. blur(7px) from above.

    (Technically SVG only makes this distinction for transform animations and it doesn't actually do list concatenation, but matrix post-multiplication which is functionally equivalent.)

    This is a very misleading summary.

    How is that misleading?

    Only certain data types are additive (number, length, angle, color, x-y pairs, and transformations). However, they normally behave identically as far as how the addition is calculated, regardless whether it is addition from separate animations or from repeats.

    The only time there is a difference is for <animateTransform>.

    Isn't that precisely what the summary says?

    Presumably, if you used <animate> to animate a transformation by directly setting the transformation string as the value (as proposed for Level 2 SVG animations), then you would always be adding the complete animation, even for accumulations.

    I don't know why that is the assumed behavior.

    (For what it's worth, as the implementer of SMIL in Gecko, the distinction between these two types of addition was very surprising and was only revealed by a single test case in the SVG 1.1 test suite that failed when I used the same definition of addition across the board. The spec does not make this distinction obvious at all and arguably it only existed in the test writer's interpretation. I don't think anything can be assumed about how it ought to work from the SVG spec. One motivating reason for choosing the component-based addition for accumulation in Web Animations is to avoid creating a very long list for repeating animation.)

  12. birtles commented on Aug 2, 2017

    @birtles
    ContributorAuthor

    The main thing that would need to be worked out is the addition rules for different data types. This would be similar to how we have interpolation rules for data types -- and with the similar understanding that some things can't be added and would be replaced instead.

    Yes, I am working on adding that to CSS Values & Units since we already use this in Web Animations.

    For property values that consist of more than a single value, there usually wouldn't be a single universal way to define how to add two declarations.

    I'm optimistic we can solve this (we have most of these complex types adding together in Gecko / Servo / Blink) by doing addition on the components. There are some cases where we fall back to non-additive just like for interpolation there are some cases where we fall back to discrete animation. As with interpolation, I think we can probably fill some of these gaps in the future without breaking content.

    (This of course assumes an easy way to declare the data types of custom properties for interpolation and addition. But that's going to happen anyway, right?)

    Right.

    I think accumulate is useful. But it doesn't fit into the !add syntax. Accumulation is a property of the animation repeat cycle, not of an individual declaration. The entire animation would either need to accumulate or not. And whether it accumulates or not would be separate from whether they underlying declarations were additive or not.

    Web Animations lets you use the accumulate behavior (i.e. adding the function parameter values rather than appending to the list) when adding independent animations.

    (I'm assuming that, for !add in a @Keyframes rule set, we are always adding on to the previous value from the cascade, and not on the previous keyframe. So it would be possible to have e.g. a 50% keyframe that was additive (so the first half of the animation is defined as a change from the base value), and then a 100% keyframe that wasn't additive (final value in the animation is the same regardless of the base value).)

    Right. This per-keyframe additive behavior is already specified in Web Animations and implemented in Firefox and Chrome (but not shipping yet).

    How do you define the amount of change which becomes the amount to add?

    I assume this is referring to how to apply accumulate behavior? If so, then this proposal is simply putting forward accumulate behavior as another type of addition independent of repetition.

    Think of two text-shadow declarations: do you add them as a list, to create two shadows? Or do you add all the individual terms. So maybe there's a !concatenate list option versus !add?

    This is precisely the difference between !add and !accumulate in this proposal.

  13. 9 remaining items

  14. faceless2 commented on Dec 11, 2019

    @faceless2

    If additive behaviour was defined on a per-property basis it could apply even where lists of items aren't natural - for example:

    <style>
    .disabled {
        text-decoration: line-through !add;
    }
    a {
       text-decoration: underline !add;
    }
    </style>
    <a class="disabled">underlined and struck-out</a>

    or, slightly more hypothetically:

    <style>
    p {
        text-spacing: trim-adjacent-to-punctuation; /* a made-up value */
    }
    :lang(fr) {
       text-spacing: punctuation !add;
    }
    </style>
    /* Remove spacing around the colon AND insert a 1/4 nbsp before */
    <p lang="fr">Punctuation : le colon</p>

    Various forms of text "fix up" keep being proposed[1][2][3][4][5][6], and additive behaviour seems like a good option for what will inevitably become an increasingly long list.

    For font-feature-settings too. Here, I was wondering how you might offer more control over the cascade than just "add to, rather than replace, the previous matched value"

    p {
        font-feature-settings: "ss01" !name swash;
    }
    .fancy {
        font-feature-settings: "ss02" !add; /* gets "ss01", "ss02" */
    }
    .noswash {
        font-feature-settings: inherit !remove swash; /* gets "ss02" */
    }

    This would require each declaration has a name field, but beyond that should be no harder than the vanilla "!add" proposal.

    [1] https://www.w3.org/TR/css-text-4/#text-spacing-property
    [2] https://www.w3.org/Style/2013/paged-media-tasks#text-fix
    [3] https://www.princexml.com/doc/css-props/#prop-prince-text-replace
    [4] https://www.antennahouse.com/product/ahf66/ahf-ext.html#axf.text-replace
    [5] https://specs.rivoal.net/css-custom-tt/
    [6] https://books.idea.whatwg.org/#character-substitution
    [7] #4246

  15. LukeTOBrien commented on Jan 13, 2020

    @LukeTOBrien

    Hello there,

    I am just a humble web dev, nothing important, but I stumbled upon your discussion and I woul like to add my views.
    It seems to me that what you want is something like (Compound Assignment in C#) Additional Assignment and subtraction assignment.

    So instead of:
    !add and !remove - In your eg: text-decoration: underline !add
    You would have:
    +: and -: - So for eg: text-decoration+: underline

    I think symbols would also benefit non-English speakers

    For animations I have also bee thinking about compounding a CSS var:
    So if you want to slide in and animate the colour:

    :root {
      --animation: ;
    }
    .animated {
      animation: var(--animation);
    }
    .slide-in {
      --animation: slidein 5s;
    }
    .colour {
      --animation: var(--animation), colour 5s;
    }

    See my pen - I have commented out the compound var expression so that the pen will work.

    Whatcha think? :-)

  16. warengonzaga commented on Jan 23, 2020

    @warengonzaga

    I'm gonna read all of this @LukeTOBrien thanks for informing the AnimateCSS community.

  17. argyleink commented on Mar 30, 2021

    @argyleink

    building an animation grudgingly in javascript, just for additive compositing. wish I could use CSS only. here to say I would have reached for it right now if I could 👍🏻

  18. Kilian commented on May 4, 2021

    @Kilian

    This would be useful for any CSS property that accepts layers, like background, text-shadow, box-shadow or combines multiple options like filter and font-feature-settings like already mentioned.

    A way to add to these, or even compose them, rather than fully overwriting them, would be great.

  19. tabatkins commented on Oct 27, 2021

    @tabatkins
    Member

    I think you added this comment to the wrong issue?

  20. GrossDesignCo commented on Jan 27, 2022

    @GrossDesignCo

    +1, this would be really useful for design systems that need to define specific properties without overriding user styles.

  21. dbaron commented on Aug 11, 2022

    @dbaron
    Member

    It's not clear to me (particularly given #1594 (comment)) whether this issue should cover all additive cascading concepts in CSS, or only those intended for animation.

    Also, for what it's worth, the earliest additive cascade proposal that I could find is https://lists.w3.org/Archives/Public/www-style/1999Oct/0025.html .

  22. korenevskiy commented on Aug 12, 2022

    @korenevskiy

    You can consider the syntax with a plus sign "+" at the very end .

    button{
    	transition: width 0.5s;
    	transition: box-shadow 0.5s +;
    	transition: background-color 1s +;
    }

    Optionally, you can consider specifying the order of the member after the plus sign "+"

    button{
    	transition: width 0.5s;
    	transition: background-color 1s +2;
    	transition: box-shadow 0.5s +1;
    }

    This is the equivalent of

    button{
    	transition: width 0.5s, box-shadow 0.5s, background-color 1s;
    }

    The description should apply to all multilayer properties. Such as :

    filter, background, transition, box-shadow, text-shadow and ...

    This will allow you to structure styles by meaning.

    button{
    /* Transition Width */
    	width: 50px;
    	transition: width 0.5s;
    /* Transition box-shadow */
    	box-shadow: 0 0 10px black;
    	transition: box-shadow 0.5s +;
    /* Transition background-color */
    	background-color: red; 
    	transition: background-color 1s +;
    }
  23. SebastianZ commented on Aug 14, 2022

    @SebastianZ
    Contributor

    It's not clear to me (particularly given #1594 (comment)) whether this issue should cover all additive cascading concepts in CSS, or only those intended for animation.

    I believe those for animation are already covered by the prior resolution to ship Web Animations with additive animation and by the animation-composition property @birtles mentioned in his initial post.

    And given the long discussion in this issue about a general solution, I think it makes sense to keep using this issue to find a general solution.

    @tabatkins Could you explain why you think @Kilian's comment belongs to a different issue?

    Regarding "all cascading concepts", we should first clarify what those different concepts are. As far as I can see, we have two general main concepts (which @birtles already mentioned in his initial post).

    Manipulating a numeric inherited value

    This means a numeric value is calculated relative to the value given by the cascade. In Brian's example this was a blur(5px) on a given blur(2px) resulting in a blur(7px). Another example would be adding 10px for the top padding to a given padding: 20px resulting in padding: 30px 20px 20px 20px.

    I guess those use cases may already be covered by #2864, calc() and/or custom properties.

    Adding a value to a list of values

    This means any kind of value is appended to (or removed from) the list of a property's value, independent of whether it is inherited or not.

    This is what Brian wrote as blur(2px) + blur(5px) resulting in blur(2px) blur(5px) and generally what people mean as adding values to a property. This is obviously also what people had in mind when suggesting additive cascade as solution for #7066.
    There it doesn't matter if the value is space-separated like in transform or filter or comma-separated like in background or box-shadow as mentioned by @Kilian.
    Other examples for them would be transform: scale(1.2) + transform: translateX(100px) = transform: scale(1.2) translateX(100px), box-shadow: 2px 0 2px red + box-shadow: -2px 0 2px blue = box-shadow: 2px 0 2px red, -2px 0 2px blue or container-type: style + container-type: size = container-type: style size.

    As mentioned above, there might also be cases in which people want to remove a value from the list of values. E.g. as we now have style containment by default (which can't be removed at the moment), people may explicitly remove it for cases they only want size containment to improve performance.

    Sebastian

  24. AdamSobieski commented on Apr 13, 2026

    @AdamSobieski

    Hello. I thought of another interesting theoretical use case for additive CSS beyond transitions and animations. If a CSS property, about, could have values of types triple and/or quad, then one would be able to form semantic graphs and datasets by means of additive CSS.

    @namespace deo url('http://purl.org/spar/deo');
    @namespace doco url('http://purl.org/spar/doco');
    @namespace rdf url('http://www.w3.org/1999/02/22-rdf-syntax-ns#'); 
    
    p
    {
        about: triple(self(), url(rdf|'type'), url(doco|'Paragraph')) !add;
    }
    
    p.introduction
    {
        about: triple(self(), url(rdf|'type'), url(deo|'Introduction')) !add;
        about: triple(self(), url('http://example.org#property'), literal('This is the introduction.', 'en')) !add;
    }
    
    p.important
    {
        about: triple(self(), url('http://example.org#property'), literal('This is important.', 'en')) !add;
    }
    <p class="introduction important">...</p>
    @prefix deo: <http://purl.org/spar/deo> .
    @prefix doco: <http://purl.org/spar/doco> .
    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
    @prefix example: <http://www.example.org#> .
    
    [ rdf:type doco:Paragraph , deo:Introduction ;
      example:property "This is the introduction."@en , "This is important."@en ] .

    Here is a more complex, intricate example with many more possibilities shown. This example illustrates how one might go about using CSS selectors to transform a simple HTML table into semantic content. The example uses a document-about property to attach semantic content to a document-level graph, rather than storing it in per-element graphs.

    @namespace rdf url('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
    @namespace example url('http://www.example.org#');
    
    table.data-1
    {
        counter-reset: row;
    
        &:not(:has(tr))
        {
            document-about: triple(self(), url(example|'hasData'), url(rdf|'nil')) !add;
        }
    
        &:has(tr)
        {
            document-about: triple(self(), url(example|'hasData'), blank('list')) !add;
        }
    
        & tr:first-of-type
        {
            document-about: triple(blank('list'), url(rdf|'first'), self()) !add;
        }
    
        & tr:last-of-type
        {
            document-about: triple(self(), url(rdf|'rest'), url(rdf|'nil')) !add;           
        }
    
        & tr as --r
        {
            counter-increment: row;
    
            document-about: triple(self(), url(rdf|'first'), blank(counter(row))) !add;
    
            & + tr
            {
                document-about: triple(var(--r), url(rdf|'rest'), self()) !add;
            }
    
            & td:nth-child(1)
            {
                document-about: triple(blank(counter(row)), url(example|'hasPart1'), content()) !add;
            }
    
            & td:nth-child(2)
            {
                document-about: triple(blank(counter(row)), url(example|'hasPart2'), content()) !add;
            }
    
            & td:nth-child(3)
            {
                document-about: triple(blank(counter(row)), url(example|'hasPart3'), content()) !add;
            }
    
            & td:nth-child(4)
            {
                document-about: triple(blank(counter(row)), url(example|'hasPart4'), content()) !add;
            }
        }
    }
    <table id="t1" class="data-1">
      <tr>
        <td>1</td>
        <td>2</td>
        <td>3</td>
        <td>4</td>
      </tr>
      <tr>
        <td>5</td>
        <td>6</td>
        <td>7</td>
        <td>8</td>
      </tr>
      <tr>
        <td>9</td>
        <td>10</td>
        <td>11</td>
        <td>12</td>
      </tr>
    </table>
    @prefix example: <http://www.example.org#> .
    
    <#t1> example:hasData (
       [ example:hasPart1 "1" ; example:hasPart2 "2" ; example:hasPart3 "3" ; example:hasPart4 "4" ]
       [ example:hasPart1 "5" ; example:hasPart2 "6" ; example:hasPart3 "7" ; example:hasPart4 "8" ]
       [ example:hasPart1 "9" ; example:hasPart2 "10" ; example:hasPart3 "11" ; example:hasPart4 "12" ]
    ) .
  25. LeaVerou commented on Apr 28, 2026

    @LeaVerou
    Member

    Something like this could also be another solution (that doesn't require parser changes, unlike some of the ideas here).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions