We needed to make parts of our site ‘sticky’, and didn’t want scrolling to get jumpy. Result: a directive which aggregates a pipeline of callbacks to create sticky elements, so that several can be on a page without problems.

Example:

Beyond being declarative, there are a lot of performance concerns here, because javascript scroll handlers usually aren’t a great idea. Especially when binding events to the document, browsers may revert from using the GPU for rendering.

It would be cool if we could do something like this (but we can’t query scroll-top):

/* won't work */
@media (scroll-top: 50px) {
  .myStickyDiv {
    position: fixed;
    top: 20px;
  }
}

To make things as snappy as possible using javascript, we aggregated all the callbacks into a pipeline, so that checking the window offset only needs to be done once, and all callbacks are be throttled.

Next steps are abstracting it out so that all events which need to listen to scroll or resize etc. can be aggregated in one place. This directive would register with that service. I’m not sure I’d fully recommend that though, as it would encourage adding a ton of events on the window…

Note that this directive relies on lodash, but could easily be refactored so it didn’t.

 1 /**
 2  * @ngdoc directive
 3  * @name simple-sticky
 4  *
 5  * @description
 6  * Simple directive to create a sticky div, which will remain fixed relative to the top of the page.
 7  *
 8  * Each time the directive is used, the callbacks will be folded into a throttled pipeline (50ms)
 9  *
10  * note - requires lodash throttle() and remove()
11  *
12  * @attr simpleSticky {number} number of pixels from top
13  *
14  * @example
15  *
16  * <div class="myStickyDiv" simple-sticky="20"></div>
17  */
18 angular.module('interface')
19 .directive('simpleSticky', function($window) {
20 
21   var windowEl = angular.element($window);
22 
23   var checks = [];
24 
25   function getYOffset() {
26     return (angular.isDefined($window.pageYOffset) ?
27             $window.pageYOffset :
28             $window.document[0].documentElement.scrollTop);
29   }
30 
31   //the function will be run
32   function addCheck (fn, $scope) {
33     checks.push(fn);
34     $scope.$on('$destroy', removeCheck);
35   }
36 
37   function removeCheck(fn) {
38     _.remove(checks, fn);
39   }
40 
41   var throttleRunChecks = _.throttle(function () {
42     var pageYOffset = getYOffset();
43 
44     angular.forEach(checks, function (fn) {
45       fn.apply(null, [pageYOffset]);
46     });
47   }, 50);
48 
49   windowEl.on('scroll resize', throttleRunChecks);
50 
51   return {
52     restrict: 'A',
53     link: function(scope, element, attrs) {
54 
55       //70 because of sticky nav at top
56       var showFromTopSticky = parseInt(attrs.simpleSticky, 10) || 70,
57         positionNormal = element.css('position'),
58         showFromTopNormal = element.css('top'),
59         startFromTop = element[0].getBoundingClientRect().top + getYOffset(),
60         isAffixed;
61 
62       //check if affix state has changed
63       function checkPosition (pageYOffset) {
64         var shouldAffix = (pageYOffset + showFromTopSticky) > startFromTop;
65        
66         if (shouldAffix !== isAffixed) {
67           isAffixed = shouldAffix;
68           handleAffixing(shouldAffix);
69         }
70       }
71 
72       //handle class changes, CSS changes
73       function handleAffixing (shouldAffix) {
74         if (shouldAffix) {
75           //don't worry - we are't triggering paint storms because these only run when cross threshold (transform isn't really appropriate)
76           element.css({
77             top: showFromTopSticky + 'px',
78             width: "inherit",
79             position: "fixed"
80           });
81         } else {
82           element.css({
83             top: showFromTopNormal,
84             position: positionNormal
85           });
86         }
87         //element.toggleClass('affix', shouldAffix)
88       }
89 
90       //register a callback, handles deregistration when pass in scope
91       addCheck(function (pageYOffset) {
92         checkPosition(pageYOffset);
93       }, scope);
94       
95       //init
96       checkPosition();
97     }
98   };
99 });

Performance aside, CSS position: sticky would be nice because we could tie it into media queries, to make it easily responsive (not that we couldn’t pass that in as an attribute here). But our needs were simpler than that.

Hope it’s useful for you!