Table (Sortable)

Table (Sortable)

This script allows a table to be sorted by the column headers, in ascending or descending order.

Please note: JAWS with Chrome browser is unable to announce the column heading association with row data



Turn on a screen reader to experience this example in action.

People, Ages, and Favorite Colors
Index Name Age Code Favorite Color
2 Hannah 28 LQdBF Blue
6 Cyan 35 gskQw Burgundy
3 Salim 7 yqU5U Green
4 Greg 45 2xdOH Orange
1 Myk 33 8piQT Purple
5 Caitlin 21 jP9sa Red

HTML Source Code

<div class="table-wrap">
    <table class="sortable" aria-readonly="true" role="grid" >
        
    <caption>People, Ages, and Favorite Colors</caption>
    <thead>
        <tr>
          <th class="num">Index</th>
          <th aria-sort="ascending">Name</th>
          <th class="num">Age</th>
          <th class="no-sort">Code</th>
          <th >Favorite Color</th>
        </tr>
      </thead>
      <tbody>
        <tr>
            <td>2</td>
            <td>Hannah</td>
            <td class="num">28</td>
            <td>LQdBF</td>
            <td >Blue</td>
          </tr>
          <tr>
            <td>6</td>
            <td>Cyan</td>
            <td class="num">35</td>
            <td>gskQw</td>
            <td >Burgundy</td>
          </tr>
          <tr>
            <td>3</td>
            <td>Salim</td>
            <td class="num">7</td>
            <td>yqU5U</td>
            <td >Green</td>
          </tr>
          <tr>
            <td>4</td>
            <td>Greg</td>
            <td class="num">45</td>
            <td>2xdOH</td>
            <td >Orange</td>
          </tr>
          <tr>
            <td>1</td>
            <td>Myk</td>
            <td class="num">33</td>
            <td>8piQT</td>
            <td >Purple</td>
          </tr>
          <tr>
            <td>5</td>
            <td>Caitlin</td>
            <td class="num">21</td>
            <td>jP9sa</td>
            <td >Red</td>
          </tr>
      </tbody>
  </table>
</div>

JavaScript Source Code

var langText = {
    "sortedByColumn": "sorted by column",
    "ascending": "ascending",
    "descending":"descending"
}

class SortableTable {
constructor(tableNode) {
  this.tableNode = tableNode;

  this.columnHeaders = tableNode.querySelectorAll('thead th');

  this.sortColumns = [];

  for (var i = 0; i < this.columnHeaders.length; i++) {
    var ch = this.columnHeaders[i];
    var buttonNode = ch.querySelector('button');
    if (buttonNode) {
      this.sortColumns.push(i);
      buttonNode.setAttribute('data-column-index', i);
      buttonNode.addEventListener('click', this.handleClick.bind(this));
    }
    else
    {
      if (ch.classList.contains("no-sort")) continue;
      var buttonNode = document.createElement("button");
      buttonNode.innerHTML = ch.innerHTML.trim();
      buttonNode.setAttribute('data-column-index', i);
      buttonNode.addEventListener('click', this.handleClick.bind(this));
      var span = document.createElement("span");
      span.setAttribute("aria-hidden", "true");
      span.classList.add("sort-arrow");
      buttonNode.appendChild(span);
      ch.innerHTML ="";
      ch.appendChild(buttonNode);
    }
  }

  this.optionCheckbox = document.querySelector(
    'input[type="checkbox"][value="show-unsorted-icon"]'
  );

  if (this.optionCheckbox) {
    this.optionCheckbox.addEventListener(
      'change',
      this.handleOptionChange.bind(this)
    );
    if (this.optionCheckbox.checked) {
      this.tableNode.classList.add('show-unsorted-icon');
    }
  }

  var span = document.createElement("span");
  span.setAttribute("aria-live", "polite");
  span.classList.add("sr-announce");
  this.tableNode.parentNode.appendChild(span);
}

setColumnHeaderSort(columnIndex) {
  if (typeof columnIndex === 'string') {
    columnIndex = parseInt(columnIndex);
  }

  for (var i = 0; i < this.columnHeaders.length; i++) {
    var ch = this.columnHeaders[i];
    var buttonNode = ch.querySelector('button');
    if (i === columnIndex) {
      var value = ch.getAttribute('aria-sort');
      if (value === 'descending') {
        ch.setAttribute('aria-sort', 'ascending');
        this.sortColumn(
          columnIndex,
          'ascending',
          ch.classList.contains('num')
        );
        var captionText = this.tableNode.caption.innerText;
        if(captionText.includes("(") ) captionText = captionText.substring(0, captionText.indexOf("("));
        captionText = captionText + " ("+ langText["sortedByColumn"]+ " '" + buttonNode.innerText +"' "+ langText["ascending"]+")"; 
        this.tableNode.caption.innerHTML = captionText;
        this.tableNode.parentNode.querySelector(".sr-announce").innerHTML = captionText;
      } else {
        ch.setAttribute('aria-sort', 'descending');
        this.sortColumn(
          columnIndex,
          'descending',
          ch.classList.contains('num')
        );
        var captionText = this.tableNode.caption.innerText;
        if(captionText.includes("(") ) captionText = captionText.substring(0, captionText.indexOf("("));
        captionText = captionText + " ("+ langText["sortedByColumn"]+ " '" + buttonNode.innerText +"' "+ langText["descending"]+")";
        this.tableNode.caption.innerHTML = captionText;
        this.tableNode.parentNode.querySelector(".sr-announce").innerHTML = captionText;

      }
    } else {
      if (ch.hasAttribute('aria-sort') && buttonNode) {
        ch.removeAttribute('aria-sort');
      }
    }
  }
}

sortColumn(columnIndex, sortValue, isNumber) {
  function compareValues(a, b) {
    if (sortValue === 'ascending') {
      if (a.value === b.value) {
        return 0;
      } else {
        if (isNumber) {
          return a.value - b.value;
        } else {
          return a.value < b.value ? -1 : 1;
        }
      }
    } else {
      if (a.value === b.value) {
        return 0;
      } else {
        if (isNumber) {
          return b.value - a.value;
        } else {
          return a.value > b.value ? -1 : 1;
        }
      }
    }
  }

  if (typeof isNumber !== 'boolean') {
    isNumber = false;
  }

  var tbodyNode = this.tableNode.querySelector('tbody');
  var rowNodes = [];
  var dataCells = [];

  var rowNode = tbodyNode.firstElementChild;

  var index = 0;
  while (rowNode) {
    rowNodes.push(rowNode);
    var rowCells = rowNode.querySelectorAll('th, td');
    var dataCell = rowCells[columnIndex];

    var data = {};
    data.index = index;
    data.value = dataCell.textContent.toLowerCase().trim();
    if (isNumber) {
      data.value = parseFloat(data.value);
    }
    dataCells.push(data);
    rowNode = rowNode.nextElementSibling;
    index += 1;
  }

  dataCells.sort(compareValues);

  // remove rows
  while (tbodyNode.firstChild) {
    tbodyNode.removeChild(tbodyNode.lastChild);
  }

  // add sorted rows
  for (var i = 0; i < dataCells.length; i += 1) {
    tbodyNode.appendChild(rowNodes[dataCells[i].index]);
  }
}

/* EVENT HANDLERS */

handleClick(event) {
  var tgt = event.currentTarget;
  this.setColumnHeaderSort(tgt.getAttribute('data-column-index'));
}

handleOptionChange(event) {
  var tgt = event.currentTarget;

  if (tgt.checked) {
    this.tableNode.classList.add('show-unsorted-icon');
  } else {
    this.tableNode.classList.remove('show-unsorted-icon');
  }
}
}

// Initialize sortable table buttons
var sortableTable
window.addEventListener('load', function () {
  sortableTable =  new SortableTable(document.querySelector('table.sortable'));
  sortableTable.setColumnHeaderSort(1);
});
//This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/

CSS Source Code

.sr-only {
    position: absolute;
    top: -30em;
  }
  
  table.sortable td,
  table.sortable th {
    padding: 0.125em 0.25em;
    width: 8em;
  }
  
  table.sortable th {
    font-weight: bold;
    border-bottom: thin solid #888;
    background-color: #aaebea;
    position: relative;
    padding: 3px;;
  }
  
  table.sortable th.no-sort {
    padding-top: 0.35em;
  }
  
  table.sortable th:nth-child(5) {
    width: 10em;
  }
  
  table.sortable th button {
    padding: 4px;
    margin: 1px;
    font-size: 100%;
    font-weight: bold;
    background: transparent;
    border: none;
    display: inline;
    right: 0;
    left: 0;
    top: 0;
    bottom: 0;
    width: 100%;
    text-align: left;
    outline: none;
    cursor: pointer;
  }
  
  table.sortable th button span {
    position: absolute;
    right: 4px;
  }
  
  table.sortable th[aria-sort="descending"] span::after {
    content: "▼ ";
    color: currentcolor;
    font-size: 100%;
    top: 0;
  }
  
  table.sortable th[aria-sort="ascending"] span::after {
    content: "▲ ";
    color: currentcolor;
    font-size: 100%;
    top: 0;
  }
  
  table.show-unsorted-icon th:not([aria-sort]) button span::after {
    content: "♢";
    color: currentcolor;
    font-size: 100%;
    position: relative;
    top: -3px;
    left: -4px;
  }
  
  table.sortable td.num {
    text-align: right;
  }
  
  table.sortable tbody tr:nth-child(odd) {
    background-color: #ddd;
  }
  
  /* Focus and hover styling */
  
  table.sortable th button:focus,
  table.sortable th button:hover {
    padding: 2px;
    border: 2px solid currentcolor;
    background-color: #e5f4ff;
  }
  
  table.sortable th button:focus span,
  table.sortable th button:hover span {
    right: 2px;
  }
  
  table.sortable th:not([aria-sort]) button:focus span::after,
  table.sortable th:not([aria-sort]) button:hover span::after {
    content: "▼";
    color: currentcolor;
    font-size: 100%;
    top: 0;
  }
  
  .sort-arrow {
      margin-right: 3px;
  }

  .sr-announce{
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}

Copy and Paste Full Page Example