data-table

Sortable and filterable semantic table composition

Compose Table, Filter Bar, Pagination, and native controls around backend-owned rows. AngularCSS supplies the dense workspace presentation while AngularTS owns local bindings and the backend owns data operations.

Example

View source
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>AngularCSS Data Table</title>
    <link rel="stylesheet" href="../../css/preflight.css" />
    <link rel="stylesheet" href="../../css/angular.css" />
    <link rel="stylesheet" href="../example.css" />
    <script src="../../js/angular-ts.umd.js"></script>
    <script src="../../js/angular-css.umd.js"></script>
  </head>
  <body
    ng-app="angular.css"
    ng-cloak
    ng-init="query=''; order='customer'; page=1; state={loading:false,error:false,stale:true,selected:false,canArchive:false}; rows=[{id:'ORD-1048',customer:'Ada Lovelace',status:'Open',total:'€1,240',selected:false},{id:'ORD-1047',customer:'Grace Hopper',status:'Review',total:'€860',selected:false},{id:'ORD-1046',customer:'Edsger Dijkstra',status:'Complete',total:'€420',selected:false},{id:'ORD-1045',customer:'Margaret Hamilton',status:'Open',total:'€2,100',selected:false}]"
    data-example="data-table-demo"
  >
    <section
      class="data-table visual-example"
      aria-labelledby="orders-title"
      aria-busy="{{ state.loading }}"
    >
      <header>
        <div>
          <h2 id="orders-title">Orders</h2>
          <p>Recent customer orders</p>
        </div>
        <form role="search" aria-label="Filter orders">
          <fieldset>
            <legend class="visually-hidden">Filters</legend>
            <label>
              <span class="visually-hidden">Search orders</span>
              <input
                type="search"
                ng-model="query"
                placeholder="Search orders"
              />
            </label>
          </fieldset>
        </form>
      </header>
      <aside
        variant="warning"
        aria-live="polite"
        ng-if="state.stale && !state.loading && !state.error"
        role="alert"
      >
        <h3>Showing cached orders</h3>
        <p>The latest refresh did not complete. Values may be out of date.</p>
        <button
          size="xs"
          variant="ghost"
          type="button"
          ng-click="state.stale=false"
        >
          Dismiss
        </button>
      </aside>
      <aside
        variant="destructive"
        aria-live="assertive"
        ng-if="state.error"
        role="alert"
      >
        <h3>Orders could not be loaded</h3>
        <p>The service returned an error. Existing filters are preserved.</p>
        <button
          size="xs"
          variant="outline"
          type="button"
          ng-click="state.error=false; state.loading=true"
        >
          Retry
        </button>
      </aside>
      <menu aria-label="Selected order actions" ng-if="state.selected">
        <output>
          <span ng-repeat="row in rows | filter:{selected:true}" ng-show="$last"
            >{{ $index + 1 }}</span
          >
          selected
        </output>
        <button size="sm" variant="outline" type="button">Export</button>
        <button
          size="sm"
          type="button"
          ng-disabled="!state.canArchive"
          aria-describedby="orders-permission-note"
        >
          Archive
        </button>
      </menu>
      <figure>
        <section class="empty" ng-if="state.loading" aria-live="polite">
          <output class="spinner" aria-label="Loading orders"></output>
          <h3>Loading orders</h3>
          <p>Current filters and selection will be preserved.</p>
        </section>
        <table ng-if="!state.loading && !state.error">
          <caption>
            Orders currently available to your account.
          </caption>
          <thead>
            <tr>
              <th scope="col">
                <input
                  type="checkbox"
                  aria-label="Select all orders"
                  ng-model="state.selectAll"
                  ng-change="rows[0].selected=state.selectAll; rows[1].selected=state.selectAll; rows[2].selected=state.selectAll; rows[3].selected=state.selectAll; state.selected=state.selectAll"
                />
              </th>
              <th scope="col">
                <button
                  variant="ghost"
                  ng-click="order=order==='id' ? '-id' : 'id'"
                >
                  Order
                </button>
              </th>
              <th scope="col">
                <button
                  variant="ghost"
                  ng-click="order=order==='customer' ? '-customer' : 'customer'"
                >
                  Customer
                </button>
              </th>
              <th scope="col">Status</th>
              <th scope="col">Total</th>
            </tr>
          </thead>
          <tbody>
            <tr
              ng-repeat="row in rows | filter:query | orderBy:order | limitTo:2:(page - 1) * 2"
              aria-selected="{{ row.selected }}"
            >
              <td>
                <input
                  type="checkbox"
                  aria-label="Select {{ row.id }}"
                  ng-model="row.selected"
                  ng-change="state.selected=rows[0].selected || rows[1].selected || rows[2].selected || rows[3].selected"
                />
              </td>
              <th scope="row">{{ row.id }}</th>
              <td>{{ row.customer }}</td>
              <td><span class="badge">{{ row.status }}</span></td>
              <td>{{ row.total }}</td>
            </tr>
          </tbody>
        </table>
        <p ng-if="!state.loading && !state.error">No matching orders.</p>
      </figure>
      <footer>
        <div>
          <output
            ><span>0</span
            ><span ng-repeat="row in rows | filter:query" ng-show="$last"
              >{{ $index + 1 }}</span
            >
            orders</output
          >
          <p id="orders-permission-note" ng-if="!state.canArchive">
            Your role can export orders but cannot archive them.
          </p>
        </div>
        <nav class="pagination" aria-label="Order pages">
          <ul>
            <li>
              <a
                href="#page-1"
                ng-click="page=1"
                aria-current="{{ page === 1 ? 'page' : 'false' }}"
                >1</a
              >
            </li>
            <li>
              <a
                href="#page-2"
                ng-click="page=2"
                aria-current="{{ page === 2 ? 'page' : 'false' }}"
                >2</a
              >
            </li>
          </ul>
        </nav>
      </footer>
    </section>
  </body>
</html>

Backend-driven states

The recipe keeps service state in authored HTML so applications can bind it to their own requests and authorization model:

StateHTML contract
LoadingSet aria-busy="true" on .data-table and replace the table region with Spinner, Skeleton, or Progress content.
EmptyKeep a direct explanatory paragraph or compose Empty after the filtered tbody has no rows.
ErrorPlace an Alert with aria-live="assertive" before the data region and retain filters for retry.
Permission limitedDisable unavailable native actions and connect the reason with aria-describedby.
Stale dataPlace a warning Alert with aria-live="polite" before the data region.
Filtering and pagingBind Filter Bar controls and Pagination links to application-owned query and page state.
Selection and bulk actionBind native checkboxes to row aria-selected and reveal one shared action menu when rows are selected.

AngularCSS styles these compositions without defining a request, cache, authorization, or data-source API.

Installation

Load the AngularCSS stylesheet. This entry needs no AngularCSS JavaScript or angular.css module dependency. Add AngularTS when using application bindings such as ng-model or ng-click. See Installation for the complete setup.

This entry uses native HTML and CSS. AngularCSS registers no runtime directive for it. Semantic table with application-owned data operations.

Anatomy

Root styling selector

  • .data-table

Semantic structure

Use .data-table on a section containing a semantic header, a figure with a native table, and an optional footer. Compose Filter Bar, Pagination, Checkbox, Button, Badge, Empty, Skeleton, and Progress without data-table part classes.

API

Attributes and state

AttributeAccessPurpose
aria-selectedAuthoredSelected row state.
aria-sortAuthoredColumn sort state: ascending, descending, none, or other.

Attributes remain authored HTML, native state, or AngularTS inputs. AngularCSS does not write element state.

CSS custom properties

VariablePurpose
--data-table-max-heightMaximum scrollable table height; defaults to 32rem.

DOM events

This component does not emit a component-specific custom event.

Native DOM events continue to work normally. AngularTS event directives such as ng-click and ng-keydown, plus the data-change model callback, remain application-owned.

Behavior

Data Table composes the native Table, Filter Bar, Pagination, and existing controls. AngularTS or the backend owns rows, sorting, filtering, selection, pagination, loading, and mutations.

AngularCSS does not replace AngularTS interpolation, bindings, structural directives, form controllers, validation, or application state.

Accessibility

Keep native table, caption, header scope, and cell relationships. Sorting controls are buttons and expose direction with aria-sort; selection uses labeled native checkboxes and row aria-selected only when needed.

Authored accessible names and relationships are preserved. Test the final composition with keyboard navigation and assistive technology because labels and content come from the application.

Customization

Target semantic elements, native state selectors, and component classes with ordinary CSS. Behavior and accessible state remain with native HTML and AngularTS; visual choices belong in the application stylesheet.

Read Customization for layer order, design tokens, state variants, and iframe demo isolation.