Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at RIA Unleashed (Nov. 2010) with: Yuk Miu
Ben Nadel at RIA Unleashed (Nov. 2010) with: Yuk Miu

CSS3 Transition Properties Are Not Inherited (In AngularJS)

By Ben Nadel on

When it comes to CSS (Cascading Style Sheets), I know enough to get some things done; but, I still have a tremendous amount of learning left to do. I have plenty of knowledge-gaps. One such gap presented itself - eventually - as I was trying to get child animations to work in AngularJS. What I eventually realized was that I had a fundamental misunderstanding of how CSS inheritance works. Specifically around properties, like transition properties, that are not inherited by default.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

First off, this is not an AngularJS-specific problem (as you might be lead to believe from the post title). This is just a byproduct of CSS behavior. It just so happens that I am exploring it in the context of AngularJS and Angular's ngAnimate module.

When it comes to CSS properties, many values are inherited. This means that those properties, like "color," inherit their value from their parent (which may inherit from its parent, and so on). Other CSS properties are, by default, not inherited. Which means that you can tell it to inherit from its parent; but, that it won't do so unless told to inherit values explicitly.

Because inherited CSS values have a "seamless cascading" effect, it's easy to forget (or to never even consider) the fact that each node still inherits directly from its parent. It's just that, in many cases, each parent automatically inherits from its parent until you get to a supplied value. When it comes to non-inherited values, though, it's this parent-dependency that through me for a loop.

With CSS3 transition properties, which are not inherited, the structure of the DOM (Document Object Model) directly affects how transitions work. Imagine that you have an animated parent node "A" that has a child node "B". If you tell "B" to inherit transition properties, it will inherit A's transition properties:

A --> B (transitions)

Now, if you wrap "B" in another unstyled node "Z", "B" will stop inheriting A's transition properties:

A !!--> Z --> B (no transitions)

The reason for this is that "B" doesn't inherit from "any ancestor"; rather, it inherits from its parent, wich is "Z" in this case. However, since "Z" wasn't explicitly told to inherit properties, "B" ends up inheriting Z's "initial values" which are likely not what "A" is using.

To get around this, you have to tell both "B" and "Z" to inherit transition properties. This way, "B" will inherit from "Z" and "Z" will inherit from "A". And, "B" will end up with A's transition properties by proxy.

To see this in action, I've created a small AngularJS demo with 3 boxes. Each box has a nested "thumb" which will animate-in when the box is mounted on the DOM. In 2 of the 3 cases, however, the "thumb" is wrapped in an intermediary container:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • CSS3 Transition Properties Are Not Inherited (In AngularJS)
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • CSS3 Transition Properties Are Not Inherited (In AngularJS)
  • </h1>
  •  
  • <p>
  • <a ng-click="toggle()">Toggle Boxes</a>
  • </p>
  •  
  • <div ng-if="isShowingBoxes" class="box">
  •  
  • <!-- This thumb is a direct child of the transitioning box. -->
  • <div class="thumb"></div>
  •  
  • </div>
  •  
  • <div ng-if="isShowingBoxes" class="box">
  •  
  • <!--
  • This thumb is a distant ancestor of the transitioning box and is
  • wrapped in an intermediary element that HAS NO STYLING whatsoever.
  • -->
  • <div class="inner">
  • <div class="thumb"></div>
  • </div>
  •  
  • </div>
  •  
  • <div ng-if="isShowingBoxes" class="box">
  •  
  • <!--
  • This thumb is a distant ancestor of the transitioning box and is
  • wrapped in an intermediary element that explicitly INHERITS all of
  • the transition properties.
  • -->
  • <div class="inner-inherit">
  • <div class="thumb"></div>
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.5.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [ "ngAnimate" ] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope ) {
  •  
  • // I determine whether or not the boxes are being included.
  • $scope.isShowingBoxes = false;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the visibility of the boxes.
  • $scope.toggle = function() {
  •  
  • $scope.isShowingBoxes = ! $scope.isShowingBoxes;
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, two of the conditional boxes contain an intermediary parent. One of the intermediary parents will inherit transition properties. The other will not. Here is the CSS (via a LESS file) that determines how CSS properties are inherited:

  • div.box {
  • background-color: #FAFAFA ;
  • border: 1px solid #CCCCCC ;
  • border-radius: 3px 3px 3px 3px ;
  • height: 50px ;
  • margin-bottom: 16px ;
  • overflow: hidden ;
  • position: relative ;
  •  
  • div.thumb {
  • background-color: #EAEAEA ;
  • border: 1px solid #CCCCCC ;
  • border-radius: 3px 3px 3px 3px ;
  • height: 30px ;
  • left: 50% ;
  • margin-left: -31px ;
  • position: absolute ;
  • top: 9px ;
  • width: 60px ;
  • }
  •  
  • // div.box -- variations.
  •  
  • &.ng-enter {
  • transition-duration: 1200ms ;
  • transition-property: left ;
  • transition-timing-function: ease ;
  •  
  • // NOTE: Does NOT inherit any of the transition properties.
  • div.inner {
  • // ... therefore, no transition properties "passed down" to children.
  • }
  •  
  • // NOTE: Inherits all the transition properties, "passes down" to children.
  • div.inner-inherit {
  • transition-delay: inherit ;
  • transition-duration: inherit ;
  • transition-property: inherit ;
  • transition-timing-function: inherit ;
  • }
  •  
  • div.thumb {
  • left: 0% ;
  • transition-delay: inherit ;
  • transition-duration: inherit ;
  • transition-property: inherit ;
  • transition-timing-function: inherit ;
  • }
  • }
  •  
  • &.ng-enter-active {
  • div.thumb {
  • left: 50% ;
  • }
  • }
  • }
  •  
  • a[ ng-click ] {
  • color: red ;
  • cursor: pointer ;
  • text-decoration: underline ;
  • user-select: none ;
  • -moz-user-select: none ;
  • -webkit-user-select: none ;
  • }

As you can see, the "thumb" is set to inherit CSS3 transition properties. However, only one of the "inner" containers is also set to inherit CSS3 transition properties. As such, 1 of the 3 "thumb" elements will fail to animate as it never inherits the outer box's transition properties:


 
 
 

 
 CSS transition properties inherit from the parent, but only when told to do so explicitly. 
 
 
 

For the experienced CSS developers out there, this might be painfully obvious. But hopefully, for the intermediate CSS developers like myself, this can help shed some light on why some of your nested animations aren't executing.




Reader Comments

@All,

Over on Twitter, @tedmasterweb asked me if this was to "spec" or if this might be a bug. Based on what I've read on the MDN (Mozilla Developer Network), I am keen on saying this is according to the CSS specifications. When you look at the transition-related CSS properties, they are defined as:

Inherited : No

And, when you look up what that means:

>> When no value for an non-inherited property (sometimes called a
>> reset property in Mozilla code) has been specified on an element,
>> the element gets the initial value of that property (as specified in
>> the property's summary).

So, if the transition properties are not set, reading said values will return the initial value of the property. Putting this in the context of the demo, if the inner-most element inherits from the proxy element and the proxy element doesn't have transition defined, it will report the initial value. As such, I believe it is to spec that the proxy container will block the top-level inheritance.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.