Skip to content
Español

Navigate

Type to search. Press Escape to close.

    Components · js

    Data table (sort and filter)

    Sorting and filtering for a table that already exists in the HTML. The server sends a normal <table class="iv-table">; on init the sortable headers become buttons with aria-sort, the filter field appears and a live status announces how many rows are visible. Nothing is paginated or virtualised: the rows you serve are the rows that get sorted, so keep the table at a size a page can hold.

    CSS: css/components/datatable.css on top of table.css (both in ivolt.css). JS: @intervolutions/ivolt/datatable or auto.js. Added in v0.2.

    Filter and sort in the browserfixtures/datatable/basic.htmlOpen alone — Filter and sort in the browser
    <div class="iv-datatable" data-iv-component="datatable">
      <div class="iv-datatable__filter">
        <label class="iv-label" for="dt-orders-q">Filter rows</label>
        <input class="iv-input" id="dt-orders-q" type="search" autocomplete="off" placeholder="Customer, city or status" data-iv-datatable-filter>
      </div>
      <div class="iv-table-wrap" role="region" aria-labelledby="dt-orders-caption" tabindex="0">
        <table class="iv-table">
          <caption id="dt-orders-caption">Sample orders. The names, cities and totals below are made up for this demo; nothing here comes from a real account.</caption>
          <thead>
            <tr>
              <th scope="col" data-iv-sort>Customer</th>
              <th scope="col" data-iv-sort="text">City</th>
              <th scope="col" data-iv-sort="date">Placed</th>
              <th scope="col" class="iv-u-text-end" data-iv-sort="number">Total (EUR)</th>
              <th scope="col">Status</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">Ada Márquez</th>
              <td>Córdoba</td>
              <td><time datetime="2026-03-04">4 Mar 2026</time></td>
              <td class="iv-u-text-end">1,240.00</td>
              <td><span class="iv-badge iv-badge--success">Shipped</span></td>
            </tr>
            <tr>
              <th scope="row">Bruno Oliveira</th>
              <td>Porto</td>
              <td><time datetime="2026-01-22">22 Jan 2026</time></td>
              <td class="iv-u-text-end">318.50</td>
              <td><span class="iv-badge">Draft</span></td>
            </tr>
            <tr>
              <th scope="row">Chiara Rossi</th>
              <td>Milano</td>
              <td><time datetime="2026-05-11">11 May 2026</time></td>
              <td class="iv-u-text-end">2,905.75</td>
              <td><span class="iv-badge iv-badge--success">Shipped</span></td>
            </tr>
            <tr>
              <th scope="row">Diego Fernández</th>
              <td>Alicante</td>
              <td><time datetime="2026-02-08">8 Feb 2026</time></td>
              <td class="iv-u-text-end">76.20</td>
              <td><span class="iv-badge iv-badge--warning">On hold</span></td>
            </tr>
            <tr>
              <th scope="row">Elin Karlsson</th>
              <td>Göteborg</td>
              <td><time datetime="2026-04-27">27 Apr 2026</time></td>
              <td class="iv-u-text-end">540.00</td>
              <td><span class="iv-badge iv-badge--success">Shipped</span></td>
            </tr>
            <tr>
              <th scope="row">Farid Haddad</th>
              <td>Marseille</td>
              <td><time datetime="2026-06-02">2 Jun 2026</time></td>
              <td class="iv-u-text-end">1,088.90</td>
              <td><span class="iv-badge iv-badge--danger">Cancelled</span></td>
            </tr>
            <tr>
              <th scope="row">Greta Hoffmann</th>
              <td>Leipzig</td>
              <td><time datetime="2026-01-09">9 Jan 2026</time></td>
              <td class="iv-u-text-end">9,410.00</td>
              <td><span class="iv-badge iv-badge--success">Shipped</span></td>
            </tr>
            <tr>
              <th scope="row">Hugo Álvarez</th>
              <td>Ourense</td>
              <td><time datetime="2026-03-30">30 Mar 2026</time></td>
              <td class="iv-u-text-end">212.40</td>
              <td><span class="iv-badge iv-badge--warning">On hold</span></td>
            </tr>
            <tr>
              <th scope="row">Ingrid Dahl</th>
              <td>Bergen</td>
              <td><time datetime="2026-05-19">19 May 2026</time></td>
              <td class="iv-u-text-end">47.00</td>
              <td><span class="iv-badge">Draft</span></td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    // data-iv-component="datatable"
    {
      datatableFilter: true,
      sort: true,
    }

    Initial sort and typed values

    data-iv-sorted="asc" or "desc" on a header sorts the table as soon as it is initialised. A cell can carry data-iv-value when the displayed text is not the value to sort by, and date cells are read from <time datetime>. Cells without a valid value go last in both directions.

    Initial sort and typed valuesfixtures/datatable/sorted.htmlOpen alone — Initial sort and typed values
    <div class="iv-datatable" data-iv-component="datatable" data-iv-status-text="Showing {visible} of {total} builds">
      <div class="iv-table-wrap" role="region" aria-labelledby="dt-builds-caption" tabindex="0">
        <table class="iv-table iv-table--striped">
          <caption id="dt-builds-caption">Made-up build report, sorted by duration on load. The last row has no duration on purpose: rows without a value sort to the end in both directions.</caption>
          <thead>
            <tr>
              <th scope="col" data-iv-sort>Job</th>
              <th scope="col" class="iv-u-text-end" data-iv-sort="number" data-iv-sorted="desc">Duration</th>
              <th scope="col" data-iv-sort="text">Result</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">bundle-css</th>
              <td class="iv-u-text-end" data-iv-value="42.5">42.5 s</td>
              <td>Passed</td>
            </tr>
            <tr>
              <th scope="row">bundle-js</th>
              <td class="iv-u-text-end" data-iv-value="18">18 s</td>
              <td>Passed</td>
            </tr>
            <tr>
              <th scope="row">unit-tests</th>
              <td class="iv-u-text-end" data-iv-value="126.75">2 min 6.75 s</td>
              <td>Passed</td>
            </tr>
            <tr>
              <th scope="row">browser-tests</th>
              <td class="iv-u-text-end" data-iv-value="394">6 min 34 s</td>
              <td>Failed</td>
            </tr>
            <tr>
              <th scope="row">size-report</th>
              <td class="iv-u-text-end" data-iv-value="7.25">7.25 s</td>
              <td>Passed</td>
            </tr>
            <tr>
              <th scope="row">pack-smoke</th>
              <td class="iv-u-text-end"></td>
              <td>Skipped</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    // data-iv-component="datatable"
    {
      statusText: "Showing {visible} of {total} builds",
      sort: true,
      sorted: "desc",
      value: 42.5,
    }

    Column types

    AttributeComparisonCell value
    data-iv-sort or data-iv-sort="text"localeCompare, numeric-aware, accent and case insensitivedata-iv-value, else the cell text
    data-iv-sort="number"numeric, after stripping units and separatorsdata-iv-value, else the cell text
    data-iv-sort="date"chronological (Date.parse)data-iv-value, else <time datetime>, else the cell text

    Options

    OptionAttributeDefaultEffect
    filterDelaydata-iv-filter-delay150Milliseconds waited after typing before the rows are filtered.
    emptyTextdata-iv-empty-textNo rows matchText of the row shown when the filter hides everything.
    statusTextdata-iv-status-text{visible} of {total} rowsTemplate of the live status; both placeholders are replaced.
    localedata-iv-localedocument languageLocale used by text comparisons.

    Methods and events

    sort(columnIndexOrTh, direction?), filter(query), clearFilter(), reset() (original order and no filter), destroy(); read sortColumn, sortDirection, rows (original order), visibleRows and query. Events on the root element: iv:sort (cancelable, detail.column, direction, previousColumn, previousDirection) then iv:sorted; iv:filter (cancelable, detail.query, previousQuery) then iv:filtered with visible and total.

    Keyboard and assistive technology

    Sort controls are real buttons, so Enter and Space activate them and their accessible name is the header text; the arrow is decorative CSS. Only the sorted column carries aria-sort. The filter is a native search field. Hidden rows use the hidden attribute, so screen readers do not read them, and the row count is announced through a role="status" element. Focus never moves when sorting or filtering.

    Without JavaScript

    The table is a plain table in the order the server sent it. The filter block is not displayed at all, so users never see a control that does nothing. Serve the rows in the most useful order.

    Common mistakes

    • Thousands of rows: this component keeps every row in the DOM. Paginate on the server or filter the data before rendering.
    • Marking a header sortable when its cells are mixed content (badges, buttons): put the sort value in data-iv-value.
    • Omitting the <caption> and the labelled scroll region: the table needs a name, with or without JavaScript.