Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
ion-list sticky headers

What this does:

Within an <ion-scroll>, this directive creates sticky item headers that get bumped out of the way by the next item.



  • Needs UnderscoreJS for its _.throttle utility.
  • Must be used within an <ion-scroll> or <ion-content>
  • You must group each header and contents together within a container element (this container element defines the area in which the header should stay).
  • Not tested with collection-repeat -- only with ng-repeat (please let me know if it works and I'll update)
  • This directive works by cloning the "sticky header" and appending it between the outer scroll container and inner scroll container -- as a sibling of the scrollbar, for reference. Thus, you might need to edit your CSS if it doesn't already apply correctly to the cloned header element.

Example: If you want to render a list of posts with sticky post titles, you could create markup like this:

<article class="post" ng-repeat="post in posts track by">
    <h1>Post Title</h1>
        <p>Long post body with many paragraphs...</p>

Then all we need to do is make sure it's within an <ion-scroll>, and add the affix-within-container=".post" attribute to the <article>. (See affixWithinContainer.html below)

Where affix-within-container=".post" tells the <h1> that it is the sticky header for everything within it's closest .post ancestor. i.e. $(element).closest('.post').

<ion-content scroll-event-interval="5"><!-- or <ion-scroll> -->
<article class="post" ng-repeat="post in posts track by post._id">
<h1 affix-within-container=".post">
.directive('affixWithinContainer', function($document, $ionicScrollDelegate) {
var transition = function(element, dy, executeImmediately) {[ionic.CSS.TRANSFORM] == 'translate3d(0, -' + dy + 'px, 0)' ||
executeImmediately ?[ionic.CSS.TRANSFORM] = 'translate3d(0, -' + dy + 'px, 0)' :
ionic.requestAnimationFrame(function() {[ionic.CSS.TRANSFORM] = 'translate3d(0, -' + dy + 'px, 0)';
return {
restrict: 'A',
require: '^$ionicScroll',
link: function($scope, $element, $attr, $ionicScroll) {
var $affixContainer = $element.closest($attr.affixWithinContainer) || $element.parent();
var top = 0;
var height = 0;
var scrollMin = 0;
var scrollMax = 0;
var scrollTransition = 0;
var affixedHeight = 0;
var updateScrollLimits = _.throttle(function(scrollTop) {
top = $affixContainer.offset().top;
height = $affixContainer.outerHeight(false);
affixedHeight = $element.outerHeight(false);
scrollMin = scrollTop + top;
scrollMax = scrollMin + height;
scrollTransition = scrollMax - affixedHeight;
}, 500, {
trailing: false
var affix = null;
var unaffix = null;
var $affixedClone = null;
var setupAffix = function() {
unaffix = null;
affix = function() {
$affixedClone = $element.clone().css({
position: 'fixed',
top: 0,
left: 0,
right: 0
var cleanupAffix = function() {
$affixedClone && $affixedClone.remove();
$affixedClone = null;
var setupUnaffix = function() {
affix = null;
unaffix = function() {
$scope.$on('$destroy', cleanupAffix);
var affixedJustNow;
var scrollTop;
$($ionicScroll.element).on('scroll', function(event) {
scrollTop = (event.detail || event.originalEvent && event.originalEvent.detail).scrollTop;
if (scrollTop >= scrollMin && scrollTop <= scrollMax) {
affixedJustNow = affix ? affix() || true : false;
if (scrollTop > scrollTransition) {
transition($affixedClone[0], Math.floor(scrollTop-scrollTransition), affixedJustNow);
} else {
transition($affixedClone[0], 0, affixedJustNow);
} else {
unaffix && unaffix();
Copy link

tangentlin commented Jan 4, 2015

This is extremely helpful and it is the only solution I have found working after trying iOSList, StickySectionHeader etc., most would not work well with mobile interface, or hard to integrate with Angular. Your solution is also simple enough that it does not even depend on any external CSS.

I have noticed that when I used the directive in ion-nav-view, somehow position: fixed does not work, but by changing it to position: absolute, it works like a charm.

Copy link

guinetik commented Feb 24, 2015

cudos man. this was very useful.

Copy link

aliok commented Apr 17, 2015

Hi @Collins,

I improved the code here and converted it into a project (bower, demos, etc).

See the project page:

It is MIT licensed and your name exists in the copyright owners.

Here are things I have done:

  • made the code more beautiful
  • documented the code
  • got rid of dependencies to Underscore.js and jQuery
  • fixed a couple of alignment issues (e.g. when there are fixed or absolute positioned elements already on top.
    or when the affix container had bottom margin)
  • published it as a Bower package so that folks can use it w/o copy-pasting the Gist and creating the file etc.
  • implemented a couple of demos

Copy link

Poordeveloper commented May 31, 2015

Try out this one, just add ion-sticky to ion-content, it will detect dividers and make the active one sticky.

Copy link

shaikhspear16 commented May 18, 2016

anyone done this for ionic 2?

Copy link

rdsmartins commented May 23, 2016

It will be perfect if we could port it to Ionic 2 ....

Copy link

rico commented Aug 10, 2016

+1 for an Ionic 2 implementation.

Copy link

jonaszuberbuehler commented Jun 18, 2017

We needed this feature in one of our projects. Created a small directive out of it: Works with Ionic 2/3 on ion-list-header, might be helpful.

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