var app = angular.module('myApp', ['anguFixedHeaderTable']);
<!DOCTYPE html>
<html ng-app="myApp">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS fixed header scrollable table directive</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
    <style>
        .table {
            height: 400px;
        }
        
        .nonHeader th {
          font-weight: normal !important;
        }
    </style>
    <script src="//code.jquery.com/jquery-2.0.3.min.js"></script>
    <script src="//code.angularjs.org/1.2.16/angular.js"></script>
    <script src="angu-fixed-header-table.js"></script>
    <script src="app.js"></script>
  </head>

  <body>
    <table class="table table-bordered" fixed-header>
        <thead>
            <tr>
                <th>Header 1</th>
                <th>Header 2</th>
                <th>Header 3</th>
                <th>Header 4</th>
            </tr>
            <tr class="nonHeader">
                <th>Header 1</th>
                <th>Header 2</th>
                <th>Header 3</th>
                <th>Header 4</th>
            </tr>
            <tr class="nonHeader">
                <th>Header 1</th>
                <th>Header 2</th>
                <th>Header 3</th>
                <th>Header 4</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="item in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]">
                <td>Row {{item}} Col 1</td>
                <td>Row {{item}} Col 2</td>
                <td>Row {{item}} Col 3</td>
                <td>Row {{item}} Col 4</td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <td>Footer 1</td>
                <td>Footer 2</td>
                <td>Footer 3</td>
                <td>Footer 4</td>
            </tr>
        </tfoot>
    </table>

    <div class="credits text-center">
      <p>
          <a href="http://pointblankdevelopment.com.au/blog/angularjs-fixed-header-scrollable-table-directive">AngularJS fixed header scrollable table directive</a>
      </p>
      <p>
        <a href="http://pointblankdevelopment.com.au">pointblankdevelopment.com.au</a>
      </p>
    </div>
  </body>

</html>
/**
 * AngularJS fixed header scrollable table directive
 * @author Jason Watmore <jason@pointblankdevelopment.com.au> (http://jasonwatmore.com)
 * @version 1.1.0
 */
(function () {
    angular.module('anguFixedHeaderTable', [])
    .directive('fixedHeader', ['$timeout', function ($timeout) {
        return {
            restrict: 'A',
            scope: {
                tableHeight: '@'
            },
            link: function ($scope, $elem, $attrs, $ctrl) {
                function isVisible(el) {
                    var style = window.getComputedStyle(el);
                    return (style.display != 'none' && el.offsetWidth !=0 );
                }

                function isTableReady() {
                    return isVisible(elem.querySelector("tbody")) && elem.querySelector('tbody tr:first-child') != null;
                }

                var elem = $elem[0];
                // wait for content to load into table and to have at least one row, tdElems could be empty at the time of execution if td are created asynchronously (eg ng-repeat with promise)
                var unbindWatch = $scope.$watch(isTableReady,
                    function (newValue, oldValue) {
                        if (newValue === true) {
                            // reset display styles so column widths are correct when measured below
                            angular.element(elem.querySelectorAll('thead, tbody, tfoot')).css('display', '')

                            // wrap in $timeout to give table a chance to finish rendering
                            $timeout(function () {
                                // set widths of columns
                                angular.forEach(elem.querySelectorAll('tr:first-child th'), function (thElem, i) {

                                    var tdElems = elem.querySelector('tbody tr:first-child td:nth-child(' + (i + 1) + ')');
                                    var tfElems = elem.querySelector('tfoot tr:first-child td:nth-child(' + (i + 1) + ')');


                                    var columnWidth = tdElems ? tdElems.offsetWidth : thElem.offsetWidth;
                                    if(tdElems) {
                                        tdElems.style.width = columnWidth + 'px';
                                    }
                                    if(thElem) {
                                        thElem.style.width = columnWidth + 'px';
                                    }
                                    if (tfElems) {
                                        tfElems.style.width = columnWidth + 'px';
                                    }
                                });

                                // set css styles on thead and tbody
                                angular.element(elem.querySelectorAll('thead, tfoot')).css('display', 'block')

                                angular.element(elem.querySelectorAll('tbody')).css({
                                    'display': 'block',
                                    'height': $scope.tableHeight || 'inherit',
                                    'overflow': 'auto'
                                });


                                // reduce width of last column by width of scrollbar
                                var tbody = elem.querySelector('tbody');
                                var scrollBarWidth = tbody.offsetWidth - tbody.clientWidth;
                                if (scrollBarWidth > 0) {
                                    // for some reason trimming the width by 2px lines everything up better
                                    scrollBarWidth -= 2;
                                    var lastColumn = elem.querySelector('tbody tr:first-child td:last-child');
                                    lastColumn.style.width = (lastColumn.offsetWidth - scrollBarWidth) + 'px';
                                }
                            });

                            //we only need to watch once
                            unbindWatch();
                        }
                    });
            }
        };
    }]);
})();