Tables
Keep tables easily scannable and think about how big they might get.
On this page
Overview
When adding a table, first consider the nature the information you're displaying. Tables are best suited to tabular data, so if that's not what you're working with, consider other design options.
To increase "scannability" it's best to observe the following guidelines:
- Follow common table setups described below to ensure functionality is always familiar to users.
- Avoid extremely wide tables that stretch beyond visible page boundaries or are full-page widths with few columns.
- Align headings with data in columns.
- Keep small screens in mind, consider oblique headers (headers at a 45-degree angle).
- In languages that read left to right, left-align text and right-align numbers.
Most of our tables use hand-crafted markup based on Bootstrap's styles. Some areas of CommCare, particularly reporting, use DataTables and are tightly integrated with the python code that generates the data.
Tables and Layout?
Due to the nature of table cells and the ability to stack them horizontally, vertically, and apply certain alignment rules to their contents, it can be very appealing to use tables to for layout.
Please do not do this.
As mentioned in the first sentence of the overview, tables are for tabular data. They are not for aligning form fields or creating page sections and layouts.
Here are pointers toward more optimal decisions:
-
Need to create vertical "columns" of sections within a page? Bootstrap 5's Grid and Column guides can help.
-
Tempted to use a table with form labels and fields? Please don't. We have great documentation on how to horizontally style forms properly.
-
"What if my data doesn't fit the grid system? Tables seem so much more flexible for things like vertical alignment and dynamic widths." If this sounds familiar, then perhaps Bootstrap 5's flex utilities are what you need.
Basic Table
Below is an example of a very basic table without any pagination or reporting style. Using a basic table is appropriate when you know the data displayed by this table will remain fairly short (no more than 25 items). As it gets longer than that, please consider using pagination.
Important: If the data displayed by a table is fully user controlled or generated with no strict limits, expect that the data will scale and eventually that table will become unusable to a certain subset of users.
To control column spacing, use the .col-(xs|sm|mg|lg)-[0-9]
classes provided by
Bootstrap 5's grid system
.
Case Type | Name | Owner | Status |
---|---|---|---|
patient | Arundhati | worker1 | open |
patient | Karan | worker4 | open |
patient | Salman | worker1 | open |
patient | Aravind | worker4 | closed |
patient | Katherine | worker3 | closed |
<table class="table table-striped table-hover"> <thead> <tr> <!-- note that the column classes are specified on the th header elements, not the td columns in the table body --> <th class="col-sm-2">Case Type</th> <th class="col-sm-4">Name</th> <th class="col-sm-4">Owner</th> <th class="col-sm-2">Status</th> </tr> </thead> <tbody> <tr> <td>patient</td> <td>Arundhati</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Karan</td> <td>worker4</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Salman</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Aravind</td> <td>worker4</td> <td>closed</td> </tr> <tr> <td>patient</td> <td>Katherine</td> <td>worker3</td> <td>closed</td> </tr> </tbody> </table>
Sectioned Table
It's often the case that multiple tables might exist on a page. If that is the case, consider using the "sectioned table" styling below.
Title for the Table Below
Case Type | Name | Owner | Status |
---|---|---|---|
patient | Arundhati | worker1 | open |
patient | Karan | worker4 | open |
patient | Salman | worker1 | open |
patient | Aravind | worker4 | closed |
patient | Katherine | worker3 | closed |
<div class="card"> <div class="card-body"> <h4 class="card-title"> Title for the Table Below </h4> <div class="card-subtitle text-body-secondary"> (Optional) Additional one-line of descriptive text that clarifies what user is seeing. </div> <!-- ideally, a spacer of at least "3" is best to separate the table from text and titles above it --> <table class="table table-striped table-hover mt-3"> <thead> <tr> <th class="col-sm-2">Case Type</th> <th class="col-sm-4">Name</th> <th class="col-sm-4">Owner</th> <th class="col-sm-2">Status</th> </tr> </thead> <tbody> <tr> <td>patient</td> <td>Arundhati</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Karan</td> <td>worker4</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Salman</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Aravind</td> <td>worker4</td> <td>closed</td> </tr> <tr> <td>patient</td> <td>Katherine</td> <td>worker3</td> <td>closed</td> </tr> </tbody> </table> </div> </div>
Report Tables (Datatables)
Reports with advanced filters on HQ, such as the ones found in the "Reports" section, all use a javascript library called DataTables to display the data. DataTables is an extensive library with built-in support for pagination, column sorting, data typing, and more (especially when you consider the ecosystem of datatables extensions).
In CommCare HQ, we have created a wrapper class around our reporting use of datatables called
GenericTabularReport
, which takes care of generating the datatables configuration in
datatables_config.js
. This was done in an effort to reduce the amount of HTML and
javascript required to create a report, as generating custom reports for projects was once very common in HQ.
With the introduction of User Configurable Reports (UCRs), this need for quickly creating these hard-coded
custom reports has since been eliminated.
As of the time of this writing, the current reporting infrastructure could use a re-examination. It is one of the oldest un-refactored parts of HQ and, therefore, contains a lot of design problems. It's best to examine this area of the codebase with a great deal of scrutiny. As the styleguide is aimed at explaining the visual side of HQ, we recommend that you visit our Read the Docs on Reporting to get a better idea of the back end implementation.
When our reporting tools were created, DataTables was still a fairly young library and had its own set of issues,
which you can see in datatables_config.js
if you examine the strangely named variables in
still in "hungarian notation" style. Thankfully, the developers of dataTables have kept later versions
backwards-compatible with this older notation, but for reference here is a
conversion guide for the older configuration
parameters and what the modern equivalents are.
datatables_config.js
is updated. Thank you.
Simple Example
Below is an example of a standalone datatable. This kind of datatable does not really exist on HQ outside a report or UCR view, as discussed above. It's mainly here as a simplified demonstration to connect the styling and javascript
This example fetches the data asynchronously to avoid having to hardcode the tabular data, however it does not do any server-side processing or pagination. Please see the documentation for Server-Side Processing with DataTables if you want to understand how to do this further.
Additionally, the example below makes use of one extension called FixedColumns
, which is used across
a few reports in HQ. This supports fixing at least one column to the left or right of the report, allowing the rest
of the tabular data to scroll underneath it when scrolling horizontally.
Stylistically, the tables with report-style advanced filters and column sorting all use the visual style described below. Tabular data that exists outside this framework of advanced filters and column sorting use the standard Bootstrap table styles. Tables that require pagination but do not have advanced filtering capabilities of the Report views should use the Paginated Table style.
@use_datatables
decorator to your view to ensure that the CSS (on all views) and javascript
(on non-requirejs views) are included.
If you are using requirejs, make sure that datatables.bootstrap
is in your list of dependencies.
If you are using the fixedColumns
extension, make sure that datatables.fixedColumns.bootstrap
is in your list of dependencies.
Title for the Table Below
Case Type | Name | Color | Big Cats | Date of Birth | Application | Opened On | Owner | Status |
---|
<div class="card card-hq-report"> <h2 class="h5 p-3 m-0"> Title for the Table Below </h2> <table id="example-datatable" class="table table-striped table-hover table-hq-report"> <thead> <tr> <th>Case Type</th> <th>Name</th> <th>Color</th> <th>Big Cats</th> <th>Date of Birth</th> <th>Application</th> <th>Opened On</th> <th>Owner</th> <th>Status</th> </tr> </thead> <tbody> </tbody> </table> </div>
import $ from 'jquery'; import initialPageData from 'hqwebapp/js/initial_page_data'; import 'datatables.net/js/jquery.dataTables'; import 'datatables.net-fixedcolumns/js/dataTables.fixedColumns'; import 'datatables.net-fixedcolumns-bs5/js/fixedColumns.bootstrap5'; $(function () { $('#example-datatable').dataTable({ // This defines the layout of the datatable and is important for getting everything to look standard dom: "frt<'d-flex mb-1'<'p-2 ps-3'i><'p-2 ps-0'l><'ms-auto p-2 pe-3'p>>", // hides the search bar, since we often use datatables with reports that have their own set of advanced filters beyond simple search filter: false, language: { // in the past we used custom javascript to find the length menu text and update it. please avoid this. opt for using the language options lengthMenu: "_MENU_ per page", }, scrollX: "100%", fixedColumns: { left: 1, }, columnDefs: [ { width: 80, targets: 0 }, ], // this just pre-fills the datatables with a large set of data. server-side pagination can be enabled with the serverSide option as well as the "processing..." label. see datatable's docs for usage details ajax: { url: initialPageData.reverse("styleguide_datatables_data"), type: "POST", }, }); });
Paginated Table
Below is an example of a paginated table. As explained in the Pagination section of this guide, it is best to review how this pagination of tabular data is actually used in HQ. The Pagination guide has a few notable examples mentions.
Note that the example below makes use of the same fake data source as the Datatables example above for stylistic demonstration only. When this component is actually used in HQ, the data that exists in the table's rows is most likely not reporting data. Most of the time, these paginated tables contain more complex objects that have additional primary and secondary actions associated with them and more complex interactions. The Web users and Mobile Workers pages showcase this component well.
Additionally, these paginated tables generally have a simple search functionality for filtering the data and often no support for column sorting. If you find yourself requiring advanced filtering and column sorting, perhaps you want to build a report. In that case, please see the datatables section above as well as our docs on Reporting.
Case Type | Name | Color | Big Cats | Date of Birth | Application | Opened On | Owner | Status |
---|---|---|---|---|---|---|---|---|
<div id="js-paginated-table-example"> <table class="table table-striped table-hover"> <thead> <tr> <th>Case Type</th> <th>Name</th> <th>Color</th> <th>Big Cats</th> <th>Date of Birth</th> <th>Application</th> <th>Opened On</th> <th>Owner</th> <th>Status</th> </tr> </thead> <tbody> <!-- ko foreach: rows --> <tr> <!-- ko foreach: $data.columns --> <td data-bind="text: $data"></td> <!-- /ko --> </tr> <!-- /ko --> </tbody> </table> <pagination data-apply-bindings="false" params=" goToPage: goToPage, slug: 'style-guide', perPage: itemsPerPage, onLoad: onPaginationLoad, totalItems: totalItems " ></pagination> </div>
import $ from 'jquery'; import ko from 'knockout'; import _ from 'underscore'; import initialPageData from 'hqwebapp/js/initial_page_data'; import 'hqwebapp/js/components/pagination'; $(function () { let rowData = function (data) { let self = {}; self.columns = ko.observableArray(data); return self; }; let paginationExample = function () { let self = {}; self.rows = ko.observableArray(); self.perPage = ko.observable(); self.totalItems = ko.observable(); self.itemsPerPage = ko.observable(); self.showLoadingSpinner = ko.observable(true); self.error = ko.observable(); self.goToPage = function (page) { $.ajax({ method: 'POST', url: initialPageData.reverse("styleguide_paginated_table_data"), data: { page: page, limit: self.itemsPerPage(), }, success: function (data) { self.showLoadingSpinner(false); self.totalItems(data.total); self.rows.removeAll(); _.each(data.rows, function (row) { self.rows.push(new rowData(row)); }); self.error(null); }, error: function () { self.showLoadingSpinner(false); self.error(gettext("Could not load users. Please try again later or report an issue if this problem persists.")); }, }); }; // Initialize with first page of data self.onPaginationLoad = function () { self.goToPage(1); }; return self; }; $("#js-paginated-table-example").koApplyBindings(paginationExample()); });