jqGridView: A jQuery Plugin for Frozen Columns and Grouped Headers in Grids

When a project demanded a grid that could freeze both columns and headers while also supproting multi-level column gropuing, none of the existing libraries—ExtJS, EasyUI, Telerik, or native jQuery Grids—offered the three features together. The only viable path was to build a lightweight jQuery plugin that does exactly that. The result is jqGridView.

Design Overview

The grid is conceptually split into four independent but synchronized panes:

  1. Frozen header cells
  2. Frozen data rows
  3. Scrollable header cells
  4. Scrollable data rows

The scrollbars live only in the fourth pane. Vertical scrolling replicates the offset to the frozen rows, while horizontal scrolling replicates the offset to the scrollable header. This keeps the frozen areas always in view.

Initial attempts with nested <table> elements failed because cell alignment across panes proved impossible. Switching to <div>-based layout solved the alignment problem and allowed pixel-perfect positioning.

Quick Start

Basic frozen columns

$(function () {
  $('#grid').jqGridView({
    lockedCols: 3,
    destroySource: true,
    rowClass: 'row-even',
    altRowClass: 'row-odd',
    hoverClass: 'row-hover'
  });
});

Grouped headers

$(function () {
  $('#grid').jqGridView({
    lockedCols: 3,
    destroySource: true,
    leftHeader: '<tr>&ltth rowspan="2" colspan="3">Unit</th></tr>',
    rightHeader: `
      <tr>
        <th colspan="3">Pending</th>
        <th colspan="3">Level 1</th>
        ...
        <th colspan="2">Level 16</th>
      </tr>`
  });
});

Row click handler

$(function () {
  $('#grid').jqGridView({
    lockedCols: 3,
    destroySource: true,
    rowClick: function (e) {
      const { cells, rowIndex, isFrozen } = e.data;
      alert(`Row ${rowIndex}, first cell: ${cells.eq(0).text()} – clicked in ${isFrozen ? 'frozen' : 'scrollable'} area`);
    }
  });
});

CSS Architecture

All visual aspects are controlled through CSS classes. Key selectors:

  • .gv-dataContent – outer wrapper
  • .gv-header-left – frozen header pane
  • .gv-header-right – scrollable header pane
  • .gv-data-left – frozen data pane
  • .gv-data-right – scrollable data pane
  • .gv-div-table, .gv-div-tr, .gv-div-th, .gv-div-td – structural divs that mimic table elements
  • .row-even, .row-odd, .row-hover – zebra striping and hover effects

Plugin Source

/**
 * jqGridView – jQuery plugin for frozen columns and grouped headers
 * Usage: $('#grid').jqGridView({ lockedCols: 2 });
 */
(function ($) {
  $.fn.jqGridView = function (clientId, opts) {
    opts = $.extend({
      lockedCols: 1,
      leftHeader: '',
      rightHeader: '',
      rowClass: '',
      altRowClass: '',
      hoverClass: '',
      emptyMsg: 'No data available',
      destroySource: true,
      hideSource: false
    }, opts);

    const $src = $('#' + clientId);
    if (!$src.length) { console.error('Grid element not found'); return; }

    /* --- helpers --- */
    const toDiv = html => html
      .replace(/<tr/g, '<div class="gv-div-tr"')
      .replace(/<\/replace(/<th|<td/g, '<div class="gv-div-th"')
      .replace(/<\/th>|<\/td>/g, '</div>');

    /* --- measure columns --- */
    const widths = $src.find('tr:first th').map((_, th) => $(th).outerWidth(true)).get();
    const totalCols = widths.length;
    const locked = Math.min(opts.lockedCols, totalCols);

    let leftWidth = 0, rightWidth = 0;
    widths.forEach((w, i) => (i < locked ? leftWidth : rightWidth) += w + 1);

    /* --- build skeleton --- */
    $src.before(`
      <div class="gv-dataContent">
        <div class="gv-header-left"></div>
        <div class="gv-header-right"></div>
        <div class="gv-data-left"></div>
        <div class="gv-data-right"></div>
      </div>`);
    const $box = $src.prev();
    const $hLeft = $box.find('.gv-header-left');
    const $hRight = $box.find('.gv-header-right');
    const $dLeft = $box.find('.gv-data-left');
    const $dRight = $box.find('.gv-data-right');

    if ($src.find('tr').length === 1) {
      $box.append(`<div class="gv-empty">${opts.emptyMsg}</div>`);
      return;
    }

    /* --- clone headers --- */
    const $hdr = $src.clone();
    $hdr.find('tr:gt(0)').remove();
    $hLeft.append(`<div class="gv-div-table">${toDiv($hdr.html())}</div>`);
    $hdr.find('th:lt(' + locked + ')').remove();
    $hRight.append(`<div class="gv-div-table">${toDiv($hdr.html())}</div>`);

    /* --- clone rows --- */
    $src.find('tr:gt(0)').each((i, tr) => {
      const $tr = $(tr);
      const $left = $tr.clone();
      $left.find('td:gt(' + (locked - 1) + ')').remove();
      const $right = $tr.clone();
      $right.find('td:lt(' + locked + ')').remove();

      const leftRow = `<div class="gv-div-tr ${i % 2 ? opts.altRowClass : opts.rowClass}">${$left.html()}</div>`;
      const rightRow = `<div class="gv-div-tr ${i % 2 ? opts.altRowClass : opts.rowClass}">${$right.html()}</div>`;

      $dLeft.append(`<div class="gv-div-table">${leftRow}</div>`);
      $dRight.append(`<div class="gv-div-table">${rightRow}</div>`);
    });

    /* --- sync scroll --- */
    $dRight.on('scroll', () => {
      $dLeft.scrollTop($dRight.scrollTop());
      $hRight.scrollLeft($dRight.scrollLeft());
    });

    if (opts.hideSource) $src.hide();
    if (opts.destroySource) $src.remove();
  };
})(jQuery);

Known Limitations

  • CSS styling is utilitarian rather than pixel-perfect.
  • Tested on IE 7–9 and Chrome; minor alignment issues may appear in Chrome.
  • Row spanning (rowspan > 1) is not supported because the layout is div-based.

Tags: jquery Grid plugin Frozen Columns Grouped Headers

Posted on Thu, 09 Jul 2026 16:33:43 +0000 by Aleirita