diff --git a/djblets/datagrid/templates/datagrid/listview.html b/djblets/datagrid/templates/datagrid/listview.html
index dc176ade2bb5891cf0c0d23eca7ef8ac6fa906a6..3ac947718257189d17ec25e89694ca91ca532dc4 100644
--- a/djblets/datagrid/templates/datagrid/listview.html
+++ b/djblets/datagrid/templates/datagrid/listview.html
@@ -8,32 +8,45 @@
 {% endblock %}
  </div>
  <div class="datagrid-main">
-  <table class="datagrid">
-   <colgroup>
+  <div class="datagrid">
+   <table class="datagrid-head">
+    <colgroup>
 {% for column in datagrid.columns %}
-    <col class="{{column.id}}"{% ifnotequal column.width 0 %} width="{{column.width}}%"{% endifnotequal %} />
+     <col class="{{column.id}}"{% ifnotequal column.width 0 %} width="{{column.width}}%"{% endifnotequal %} />
 {% endfor %}
-    <col class="datagrid-customize" />
-   </colgroup>
-   <thead>
-    <tr class="datagrid-headers">
+     <col class="datagrid-customize" />
+    </colgroup>
+   </table>
+
+   <div class="datagrid-body-container">
+    <table class="datagrid-body">
+     <colgroup>
 {% for column in datagrid.columns %}
-     {{column.get_header}}{% endfor %}
-     <th class="edit-columns datagrid-header" id="{{datagrid.id}}-edit"><div class="datagrid-icon datagrid-icon-edit" title="{% trans "Edit columns" %}"></div></th>
-    </tr>
-   </thead>
-   <tbody>
+      <col class="{{column.id}}"{% ifnotequal column.width 0 %} width="{{column.width}}%"{% endifnotequal %} />
+{% endfor %}
+      <col class="datagrid-customize" />
+     </colgroup>
+     <thead>
+      <tr class="datagrid-headers">
+{% for column in datagrid.columns %}
+       {{column.get_header}}{% endfor %}
+       <th class="edit-columns datagrid-header" id="{{datagrid.id}}-edit"><div class="datagrid-icon datagrid-icon-edit" title="{% trans "Edit columns" %}"></div></th>
+      </tr>
+     </thead>
+     <tbody>
 {% for row in datagrid.rows %}
-    <tr class="{% cycle odd,even %}">
+      <tr class="{% cycle odd,even %}">
 {%  for cell in row.cells %}
-     {{cell}}{% endfor %}
-    </tr>
+       {{cell}}{% endfor %}
+      </tr>
 {% endfor %}
-   </tbody>
-  </table>
+     </tbody>
+    </table>
+   </div>
 {% if is_paginated %}
 {%  paginator %}
 {% endif %}
+  </div>
  </div>
  <table class="datagrid-menu" id="{{datagrid.id}}-menu" style="display:none;position:absolute;">
 {% for column in datagrid.all_columns %}
diff --git a/djblets/static/djblets/css/datagrid.less b/djblets/static/djblets/css/datagrid.less
index c7b92e2038f922cb088aae8be0e57e6802aa6a25..c16d80bb10b7133b2e8ff558758c5a4ca4b5af53 100644
--- a/djblets/static/djblets/css/datagrid.less
+++ b/djblets/static/djblets/css/datagrid.less
@@ -242,6 +242,11 @@
   }
 }
 
+.datagrid-head,
+.datagrid-body {
+  border-collapse: collapse;
+}
+
 .datagrid-menu {
   background-color: #F0F0F0;
   border-left: 1px #303030 solid;
diff --git a/djblets/static/djblets/js/datagrid.js b/djblets/static/djblets/js/datagrid.js
index 9d8ec588312b9a2669544f9a5f2433a84c803c86..92d37a10f5995f9c17481336ba35a491cd382112 100644
--- a/djblets/static/djblets/js/datagrid.js
+++ b/djblets/static/djblets/js/datagrid.js
@@ -11,14 +11,23 @@
  * Creates a datagrid. This will cause drag and drop and column
  * customization to be enabled.
  */
-$.fn.datagrid = function() {
+$.fn.datagrid = function(options) {
     var $grid = this,
         gridId = this.attr("id"),
-        $editButton = $("#" + gridId + "-edit"),
         $menu = $("#" + gridId + "-menu"),
         $summaryCells = $grid.find("td.summary"),
+        $gridContainer = $grid.find('.datagrid'),
+        $bodyContainer = $gridContainer.find('.datagrid-body-container'),
+        $headTable = $gridContainer.find('.datagrid-head'),
+        $bodyTable = $bodyContainer.find('.datagrid-body'),
+        $bodyTableHead = $bodyTable.find('thead'),
+        $paginator = $gridContainer.find('.paginator'),
+        autoFitGrid = ($grid.css('height') !== undefined),
+        $window = $(window),
+        $editButton,
 
         /* State */
+        storedColWidths = [],
         activeColumns = [],
         $activeMenu = null,
         columnMidpoints = [],
@@ -26,39 +35,14 @@ $.fn.datagrid = function() {
         dragColumnsChanged = false,
         dragColumnWidth = 0,
         dragIndex = 0,
-        dragLastX = 0;
+        dragLastX = 0,
+        lastWindowWidth;
 
-    $grid.data('datagrid', this);
+    options = options || {};
 
-    /* Add all the non-special columns to the list. */
-    $grid.find("col").not(".datagrid-customize").each(function(i, col) {
-        activeColumns.push(col.className);
-    });
+    $grid.data('datagrid', this);
 
-    $grid.find("th")
-        /* Make the columns unselectable. */
-        .disableSelection()
-
-        /* Make the columns draggable. */
-        .not(".edit-columns").draggable({
-            appendTo: "body",
-            axis: "x",
-            containment: $grid.find("thead:first"),
-            cursor: "move",
-            helper: function() {
-                var $el = $(this);
-
-                return $("<div/>")
-                    .addClass("datagrid-header-drag datagrid-header")
-                    .width($el.width())
-                    .height($el.height())
-                    .css("top", $el.offset().top)
-                    .html($el.html());
-            },
-            start: startColumnDrag,
-            stop: endColumnDrag,
-            drag: onColumnDrag
-        });
+    setupHeader();
 
     /* Register callbacks for the columns. */
     $menu.find("tr").each(function(i, row) {
@@ -70,11 +54,6 @@ $.fn.datagrid = function() {
             });
     });
 
-    $editButton.click(function(evt) {
-        evt.stopPropagation();
-        toggleColumnsMenu();
-    });
-
     /*
      * Attaches click event listener to all summary td elements,
      * following href of child anchors if present.  This is being
@@ -93,18 +72,8 @@ $.fn.datagrid = function() {
 
     $(document.body).click(hideColumnsMenu);
 
-    $grid.find('.datagrid-header-checkbox').change(function(evt) {
-        /*
-         * Change the checked state of all matching checkboxes to reflect
-         * the state of the checkbox in the header.
-         */
-        var $checkbox = $(this),
-            colName = $checkbox.data('checkbox-name');
-
-        $grid.find('tbody input[data-checkbox-name="' + colName + '"]')
-            .prop('checked', $checkbox.prop('checked'))
-            .change();
-    });
+    $window.resize(onResize);
+    onResize();
 
 
     /********************************************************************
@@ -114,6 +83,179 @@ $.fn.datagrid = function() {
         loadFromServer(null, true);
     };
 
+    /*
+     * Resizes the table body to fit into the datagrid's allocated height.
+     *
+     * This requires a fixed height on the datagrid, which must be
+     * done by the caller.
+     */
+    this.resizeToFit = function() {
+        $bodyContainer.height($grid.innerHeight() -
+                              $bodyContainer.position().top -
+                              ($paginator.outerHeight() || 0));
+
+        syncColumnSizes();
+    };
+
+
+    /********************************************************************
+     * Layout
+     ********************************************************************/
+
+    /*
+     * Sets up the table header.
+     *
+     * This pulls out the table header into its own table, stores elements
+     * and state, and hooks up events.
+     *
+     * We create a separate table for the header in order to allow for the
+     * table body to scroll without scrolling the header. This requires
+     * that a caller sets a fixed height for the datagrid and calls
+     * resizeToFit on window resize.
+     */
+    function setupHeader() {
+        var $origHeader = $bodyTable.find('thead');
+
+        /* Store the original widths of the colgroup columns. */
+        $bodyTable.find('colgroup col').each(function(i, colEl) {
+            storedColWidths.push(colEl.width);
+        });
+
+        /* Create a copy of the header and place it in a separate table. */
+        $headTable
+            .find('thead')
+                .remove()
+            .end()
+            .append($origHeader.clone().show());
+        $origHeader.hide();
+
+        activeColumns = [];
+
+        /* Add all the non-special columns to the list. */
+        $headTable.find("col").not(".datagrid-customize")
+            .each(function(i, col) {
+                activeColumns.push(col.className);
+            });
+
+        $headTable.find("th")
+            /* Make the columns unselectable. */
+            .disableSelection()
+
+            /* Make the columns draggable. */
+            .not(".edit-columns").draggable({
+                appendTo: "body",
+                axis: "x",
+                containment: $headTable.find("thead:first"),
+                cursor: "move",
+                helper: function() {
+                    var $el = $(this);
+
+                    return $("<div/>")
+                        .addClass("datagrid-header-drag datagrid-header")
+                        .width($el.width())
+                        .height($el.height())
+                        .css("top", $el.offset().top)
+                        .html($el.html());
+                },
+                start: startColumnDrag,
+                stop: endColumnDrag,
+                drag: onColumnDrag
+            });
+
+        $editButton = $("#" + gridId + "-edit")
+            .click(function(evt) {
+                evt.stopPropagation();
+                toggleColumnsMenu();
+            });
+
+        $headTable.find('.datagrid-header-checkbox').change(function(evt) {
+            /*
+             * Change the checked state of all matching checkboxes to reflect
+             * the state of the checkbox in the header.
+             */
+            var $checkbox = $(this),
+                colName = $checkbox.data('checkbox-name');
+
+            $bodyTable.find('tbody input[data-checkbox-name="' + colName + '"]')
+                .prop('checked', $checkbox.prop('checked'))
+                .change();
+        });
+    }
+
+    /*
+     * Synchronizes the column sizes between the header and body tables.
+     *
+     * Since we have two tables that we're pretending are one, we need to
+     * make sure the columns line up properly. This performs that work by
+     * doing the following:
+     *
+     * 1) Reset the main table back to the defaults we had when the datagrid
+     *    was first created.
+     *
+     * 2) Temporarily show the "real" header, so we can calculate all the
+     *    widths.
+     *
+     * 3) Calculate all the new widths for the colgroups for both tables,
+     *    taking into account the scrollbar.
+     *
+     * 4) Set the widths to their new values.
+     */
+    function syncColumnSizes() {
+        var origHeaderCells = $bodyTableHead[0].rows[0].cells,
+            $fixedCols = $headTable.find('colgroup col'),
+            $origCols = $bodyTable.find('colgroup col'),
+            numCols = origHeaderCells.length,
+            bodyWidths = [],
+            headWidths = [],
+            extraWidth = 0,
+            width,
+            i;
+
+        /* First, unset all the widths and restore to defaults. */
+        for (i = 0; i < numCols; i++) {
+            $origCols[i].width = storedColWidths[i];
+        }
+
+        /* Store all the widths we'll apply. */
+        $bodyTableHead.show();
+
+        $headTable.width($bodyContainer.width() - 1);
+        extraWidth = $bodyContainer.width() - $bodyTable.width();
+
+        for (i = 0; i < numCols; i++) {
+            width = $(origHeaderCells[i]).outerWidth();
+            bodyWidths.push(width);
+            headWidths.push(width);
+        }
+
+        $bodyTableHead.hide();
+
+        /* Modify the widths to account for the scrollbar and extra spacing */
+        headWidths[numCols - 1] = bodyWidths[numCols - 1] - 1;
+        headWidths[numCols - 2] = bodyWidths[numCols - 2] - 1 + extraWidth;
+
+        /* Now set the new state. */
+        for (i = 0; i < numCols; i++) {
+            $origCols[i].width = bodyWidths[i];
+            $fixedCols[i].width = headWidths[i];
+        }
+    }
+
+    /*
+     * Handles window resizes.
+     *
+     * If resizing horizontally, the column widths will be synced up again.
+     */
+    function onResize() {
+        var windowWidth = $window.width();
+
+        if (windowWidth !== lastWindowWidth) {
+            lastWindowWidth = windowWidth;
+
+            syncColumnSizes();
+        }
+    }
+
 
     /********************************************************************
      * Server communication
@@ -131,8 +273,12 @@ $.fn.datagrid = function() {
         $.get(url, function(html) {
             if (reloadGrid) {
                 $grid.replaceWith(html);
-                $("#" + gridId).datagrid();
+                $grid = $("#" + gridId).datagrid();
+            } else {
+                setupHeader();
             }
+
+            $grid.trigger('reloaded');
         });
     }
 
@@ -326,7 +472,7 @@ $.fn.datagrid = function() {
         /* Clear and rebuild the list of mid points. */
         columnMidpoints = [];
 
-        $grid.find("th").not(".edit-columns").each(function(i, column) {
+        $headTable.find("th").not(".edit-columns").each(function(i, column) {
             var $column = $(column),
                 offset = $column.offset();
 
@@ -358,18 +504,7 @@ $.fn.datagrid = function() {
      *                          before.
      */
     function swapColumnBefore(index, beforeIndex) {
-        /* Swap the column info. */
-        var colTags = $grid.find("col"),
-            tempName,
-            table,
-            rowsLen,
-            i,
-            row,
-            cell,
-            beforeCell,
-            tempColSpan;
-
-        $(colTags[index]).insertBefore($(colTags[beforeIndex]));
+        var tempName;
 
         /* Swap the list of active columns */
         tempName = activeColumns[index];
@@ -377,7 +512,25 @@ $.fn.datagrid = function() {
         activeColumns[beforeIndex] = tempName;
 
         /* Swap the cells. This will include the headers. */
-        table = $grid.find("table:first")[0];
+        swapColumns($bodyTable[0], beforeIndex, index);
+        swapColumns($headTable[0], beforeIndex, index);
+
+        dragColumnsChanged = true;
+
+        /* Everything has changed, so rebuild our view of things. */
+        buildColumnInfo();
+    }
+
+    function swapColumns(table, beforeIndex, index) {
+        var beforeCell,
+            tempColSpan,
+            colTags = $(table).find('colgroup col'),
+            row,
+            cell,
+            rowsLen,
+            i;
+
+        colTags.eq(index).insertBefore(colTags.eq(beforeIndex));
 
         for (i = 0, rowsLen = table.rows.length; i < rowsLen; i++) {
             row = table.rows[i];
@@ -391,11 +544,6 @@ $.fn.datagrid = function() {
             cell.colSpan = beforeCell.colSpan;
             beforeCell.colSpan = tempColSpan;
         }
-
-        dragColumnsChanged = true;
-
-        /* Everything has changed, so rebuild our view of things. */
-        buildColumnInfo();
     }
 
     return $grid;
