v1.6.x-dev NDS IQ v6

Tables - National Design System

Structured data presentation with built-in sorting, row selection, responsive scrolling, and pagination for datasets of any size

Standard Table

The default table layout with striped rows and rounded borders. Toggle Interactive to preview the optional hover state

Name Email Role Status
Ahmed Al-Rashidi ahmed.rashidi@gov.sa Senior Developer Active
Fatima Al-Harbi fatima.harbi@gov.sa UX Designer Active
Sara Al-Dosari sara.dosari@gov.sa Marketing Lead Active
Layla Al-Qahtani layla.qahtani@gov.sa HR Specialist Active
<table class="nds-table"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Role</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>Ahmed Al-Rashidi</td> <td>ahmed.rashidi@gov.sa</td> <td>Senior Developer</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>Fatima Al-Harbi</td> <td>fatima.harbi@gov.sa</td> <td>UX Designer</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>Sara Al-Dosari</td> <td>sara.dosari@gov.sa</td> <td>Marketing Lead</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>Layla Al-Qahtani</td> <td>layla.qahtani@gov.sa</td> <td>HR Specialist</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> </tbody> </table>

Sortable Table

Click any column header to cycle through ascending, descending, and original order

Department
Employees
Budget
Status
Information Technology 25 5,625,000 SAR Active
Human Resources 12 3,000,000 SAR Active
Sales 18 4,500,000 SAR Under Review
Marketing 8 2,250,000 SAR Planning
<table class="nds-table nds-sortable"> <thead> <tr> <th> <div class="nds-col-header"> <span class="nds-label">Department</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by department"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th data-state="sorted-asc"> <div class="nds-col-header"> <span class="nds-label">Employees</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by employees"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">Budget</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by budget"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>Information Technology</td> <td>25</td> <td>5,625,000 SAR</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>Human Resources</td> <td>12</td> <td>3,000,000 SAR</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>Sales</td> <td>18</td> <td>4,500,000 SAR</td> <td><span class="nds-tag nds-sm" data-status="warning"><span class="nds-label">Under Review</span></span></td> </tr> <tr> <td>Marketing</td> <td>8</td> <td>2,250,000 SAR</td> <td><span class="nds-tag nds-sm" data-status="info"><span class="nds-label">Planning</span></span></td> </tr> </tbody> </table>

Table with Feedback Icons

Status columns using feedback icons for quick visual scanning of row states

Ticket ID Customer Issue Type Priority Status
#TK-001
Omar Al-Ahmad Login Issue Low
#TK-002
Layla Al-Mansouri Performance Medium
#TK-003
Yusuf Al-Kindi Data Loss High
#TK-004
Aisha Al-Farisi Feature Request Low
<table class="nds-table"> <thead> <tr> <th>Ticket ID</th> <th>Customer</th> <th>Issue Type</th> <th>Priority</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <div class="ticket-id"> <span class="ticket-number">#TK-001</span> </div> </td> <td>Omar Al-Ahmad</td> <td>Login Issue</td> <td> <span class="nds-tag nds-gray nds-sm"> <span class="nds-label">Low</span> </span> </td> <td> <span class="nds-feedback" data-status="success"> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> </span> </td> </tr> <tr> <td> <div class="ticket-id"> <span class="ticket-number">#TK-002</span> </div> </td> <td>Layla Al-Mansouri</td> <td>Performance</td> <td> <span class="nds-tag nds-yellow nds-sm"> <span class="nds-label">Medium</span> </span> </td> <td> <span class="nds-feedback" data-status="warning"> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> </span> </td> </tr> <tr> <td> <div class="ticket-id"> <span class="ticket-number">#TK-003</span> </div> </td> <td>Yusuf Al-Kindi</td> <td>Data Loss</td> <td> <span class="nds-tag nds-red nds-sm"> <span class="nds-label">High</span> </span> </td> <td> <span class="nds-feedback" data-status="error"> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> </span> </td> </tr> <tr> <td> <div class="ticket-id"> <span class="ticket-number">#TK-004</span> </div> </td> <td>Aisha Al-Farisi</td> <td>Feature Request</td> <td> <span class="nds-tag nds-gray nds-sm"> <span class="nds-label">Low</span> </span> </td> <td> <span class="nds-feedback" data-status="info"> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> </span> </td> </tr> </tbody> </table>

Table with Selection

Row checkboxes with a select-all header for bulk operations. The header checkbox shows an indeterminate state when some rows are selected, and with an active Filter both it and select-all operate on the filtered rows only (selections made before filtering persist on hidden rows). Pair with the Export component to ship files of the selected rows, and the Selection counter to show how many are selected.

Name Email Department Status
Hassan Al-Mukhtar hassan.almukhtar@moi.gov.sa Engineering Active
Nadia Al-Khatib nadia.alkhatib@moi.gov.sa Design Active
Tariq Al-Sudairi tariq.alsudairi@moi.gov.sa Marketing Pending
Zara Al-Habib zara.alhabib@moi.gov.sa Sales On Leave
<div class="nds-toolbar"> <div class="nds-bar-end"> <!-- Checklist is built from the table's own <thead> --> <div class="nds-dropmenu" data-columns-target="selectionTable"> <button class="nds-btn nds-neutral nds-md nds-menu-btn nds-dropmenu-trigger" type="button"> <i class="nds-icon nds-hgi-view-off-slash" aria-hidden="true"></i> <span class="nds-label">Columns</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <fieldset class="nds-form-group nds-check-group nds-dropmenu-group" data-columns-list data-no-auto-close> <legend class="nds-label">Visible columns</legend> </fieldset> </div> </div> </div> <div class="nds-export nds-btn-group"> <button type="button" class="nds-btn nds-secondary-outline nds-md" data-export="csv" data-export-target="#selectionTable"> <span class="nds-label">CSV</span> </button> <button type="button" class="nds-btn nds-secondary-outline nds-md" data-export="xls" data-export-target="#selectionTable"> <span class="nds-label">Excel</span> </button> <button type="button" class="nds-btn nds-secondary-outline nds-md" data-export="pdf" data-export-target="#selectionTable"> <span class="nds-label">PDF</span> </button> </div> </div> </div> <table id="selectionTable" class="nds-table" data-export-name="selection"> <thead> <tr> <th> <div class="nds-form-container nds-check-container"> <div class="nds-form-control"> <input type="checkbox" class="nds-check" aria-label="Select all rows"> </div> </div> </th> <th>Name</th> <th>Email</th> <th>Department</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <div class="nds-form-container nds-check-container"> <div class="nds-form-control"> <input type="checkbox" class="nds-check" aria-label="Select Hassan Al-Mukhtar"> </div> </div> </td> <td>Hassan Al-Mukhtar</td> <td>hassan.almukhtar@moi.gov.sa</td> <td>Engineering</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> <tr data-state="selected"> <td> <div class="nds-form-container nds-check-container"> <div class="nds-form-control"> <input type="checkbox" class="nds-check" checked aria-label="Select Nadia Al-Khatib"> </div> </div> </td> <td>Nadia Al-Khatib</td> <td>nadia.alkhatib@moi.gov.sa</td> <td>Design</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> <tr> <td> <div class="nds-form-container nds-check-container"> <div class="nds-form-control"> <input type="checkbox" class="nds-check" aria-label="Select Tariq Al-Sudairi"> </div> </div> </td> <td>Tariq Al-Sudairi</td> <td>tariq.alsudairi@moi.gov.sa</td> <td>Marketing</td> <td><span class="nds-tag nds-yellow nds-sm"><span class="nds-label">Pending</span></span></td> </tr> <tr data-state="selected"> <td> <div class="nds-form-container nds-check-container"> <div class="nds-form-control"> <input type="checkbox" class="nds-check" checked aria-label="Select Zara Al-Habib"> </div> </div> </td> <td>Zara Al-Habib</td> <td>zara.alhabib@moi.gov.sa</td> <td>Sales</td> <td><span class="nds-tag nds-gray nds-sm"><span class="nds-label">On Leave</span></span></td> </tr> </tbody> </table>

Center Aligned Table

Center-align all cell content when the data benefits from symmetrical presentation. To align a single column instead, put data-align="center|start|end" on its <th>.

Quarter Revenue Growth Status
Q1 2024 2,450,000 SAR +12% On Track
Q2 2024 2,780,000 SAR +13.5% On Track
Q3 2024 2,610,000 SAR -6.1% At Risk
Q4 2024 3,100,000 SAR +18.8% On Track
<table class="nds-table nds-center"> <thead> <tr> <th>Quarter</th> <th>Revenue</th> <th>Growth</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>Q1 2024</td> <td>2,450,000 SAR</td> <td>+12%</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">On Track</span></span></td> </tr> <tr> <td>Q2 2024</td> <td>2,780,000 SAR</td> <td>+13.5%</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">On Track</span></span></td> </tr> <tr> <td>Q3 2024</td> <td>2,610,000 SAR</td> <td>-6.1%</td> <td><span class="nds-tag nds-sm" data-status="warning"><span class="nds-label">At Risk</span></span></td> </tr> <tr> <td>Q4 2024</td> <td>3,100,000 SAR</td> <td>+18.8%</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">On Track</span></span></td> </tr> </tbody> </table>
Per-Column Alignment
Quarter Revenue Growth Status
Q1 2024 2,450,000 SAR +12% On Track
Q2 2024 2,780,000 SAR +13.5% On Track
Q3 2024 2,610,000 SAR -6.1% At Risk
<table class="nds-table"> <thead> <tr> <th>Quarter</th> <th data-align="end">Revenue</th> <th data-align="center">Growth</th> <th data-align="center">Status</th> </tr> </thead> <tbody> <tr> <td>Q1 2024</td> <td>2,450,000 SAR</td> <td>+12%</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">On Track</span></span></td> </tr> <tr> <td>Q2 2024</td> <td>2,780,000 SAR</td> <td>+13.5%</td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">On Track</span></span></td> </tr> <tr> <td>Q3 2024</td> <td>2,610,000 SAR</td> <td>-6.1%</td> <td><span class="nds-tag nds-sm" data-status="warning"><span class="nds-label">At Risk</span></span></td> </tr> </tbody> </table>

Loading State

A shimmer animation on table cells indicates data is being fetched

Name Email Department Status
Loading... Loading... Loading... Loading...
Loading... Loading... Loading... Loading...
Loading... Loading... Loading... Loading...
<table class="nds-table" data-state="loading"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Department</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> </tr> <tr> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> </tr> <tr> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> <td>Loading...</td> </tr> </tbody> </table>

Responsive Table

All tables are responsive by default. JS auto-wraps every nds-table in an nds-table-wrapper with horizontal scroll on overflow. Add nds-mask to opt into gradient fade masks on the overflow edges. Use --max-width to constrain wrapper width and --min-width to lock the table's minimum width. If --min-width is not set, the JS auto-calculates it from the table's natural content width so cells never shrink.

Employee ID Full Name Email Address Department Position Start Date Status
EMP-001 Ahmed Al-Rashidi ahmed.rashidi@gov.sa Engineering Senior Developer 2023-01-15 Active
EMP-002 Fatima Al-Harbi fatima.harbi@gov.sa Design UX Designer 2023-02-15 Active
EMP-003 Sara Al-Dosari sara.dosari@gov.sa Marketing Marketing Lead 2023-03-15 Active
EMP-004 Layla Al-Qahtani layla.qahtani@gov.sa Human Resources HR Specialist 2023-04-15 Active
<table class="nds-table" style="--max-width: 600px;"> <thead> <tr> <th>Employee ID</th> <th>Full Name</th> <th>Email Address</th> <th>Department</th> <th>Position</th> <th>Start Date</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>EMP-001</td> <td>Ahmed Al-Rashidi</td> <td>ahmed.rashidi@gov.sa</td> <td>Engineering</td> <td>Senior Developer</td> <td>2023-01-15</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>EMP-002</td> <td>Fatima Al-Harbi</td> <td>fatima.harbi@gov.sa</td> <td>Design</td> <td>UX Designer</td> <td>2023-02-15</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>EMP-003</td> <td>Sara Al-Dosari</td> <td>sara.dosari@gov.sa</td> <td>Marketing</td> <td>Marketing Lead</td> <td>2023-03-15</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> <tr> <td>EMP-004</td> <td>Layla Al-Qahtani</td> <td>layla.qahtani@gov.sa</td> <td>Human Resources</td> <td>HR Specialist</td> <td>2023-04-15</td> <td><span class="nds-tag nds-green nds-sm"><span class="nds-label">Active</span></span></td> </tr> </tbody> </table> <!-- All nds-table elements are responsive by default --> <!-- JS auto-wraps in nds-table-wrapper with scroll detection --> <!-- Add class="nds-table nds-mask" to opt into gradient fade masks on overflow edges --> <!-- --max-width: constrains the wrapper width --> <!-- style="--max-width: 600px;" - Fixed pixel width --> <!-- style="--max-width: 80%;" - Percentage of parent --> <!-- No --max-width - Full width (100%) --> <!-- --min-width: locks the table's minimum width (cells won't shrink below this) --> <!-- style="--min-width: 900px;" - Explicit minimum width --> <!-- No --min-width - Auto-calculated from content (default) -->

Table with Pagination

Large datasets can be paginated using the data-auto-pagination attribute. Add nds-page-item class to each <tr>, then put nds-paged-content and --per-page on the <tbody> itself, so the rows show their skeleton until pagination initializes.

Showing 00 of 0 service(s)
#
Service
System
Popularity
1 Identity Verification Identity & Records Most Used
2 Passport Renewal Identity & Records Most Used
3 Birth Certificate Request Identity & Records Standard
4 Marriage Contract Registration Identity & Records Standard
5 Driver's License Services Transport & Vehicles Most Used
6 Vehicle Registration Transport & Vehicles Most Used
7 Visa Application Transport & Vehicles Standard
8 Health Insurance Enrollment Healthcare & Social Most Used
9 Medical Appointment Booking Healthcare & Social Most Used
10 Employment Certificate Healthcare & Social Most Used
11 Work Permit Processing Healthcare & Social Standard
12 Retirement Benefits Application Healthcare & Social Standard
13 Tax Declaration Filing Business & Finance Most Used
14 VAT Registration Business & Finance Standard
15 Business License Application Business & Finance Standard
16 Property Registration Business & Finance Standard
17 Building Permit Request Business & Finance Standard
18 School Enrollment Education & Justice Most Used
19 Certificate Authentication Education & Justice Standard
20 Court Case Filing Education & Justice Standard
<div id="tables_pagination_demo"> <div class="nds-toolbar"> <div class="nds-bar-row"> <div class="nds-bar-start"> <span class="nds-bar-text" data-paged-target="paginatedTableBody">Showing <b data-paged-from>0</b>&ndash;<b data-paged-to>0</b> of <b data-paged-count>0</b> service(s)</span> </div> <div class="nds-bar-end"> <div class="nds-dropmenu nds-filter" data-filter-target="paginatedTableBody" data-portal> <button class="nds-btn nds-neutral nds-menu-btn nds-filter-btn nds-dropmenu-trigger"> <i class="hgi hgi-stroke hgi-filter"></i> <span class="nds-label" data-hidden="sm sr">Filter</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <div data-filter="system" data-filter-type="checkbox" data-filter-legend="System" data-filter-values='{"Identity & Records":"Identity & Records","Transport & Vehicles":"Transport & Vehicles","Healthcare & Social":"Healthcare & Social","Business & Finance":"Business & Finance","Education & Justice":"Education & Justice"}' data-no-auto-close> </div> <hr class="nds-divider"> <div data-filter="popularity" data-filter-type="radio" data-filter-legend="Popularity" data-filter-values='{"most_used":"Most Used","standard":"Standard"}' data-no-auto-close> </div> </div> <div class="nds-dropmenu-footer"> <hr class="nds-divider"> <div class="nds-dropmenu-action"> <button class="nds-btn nds-secondary nds-dropmenu-item" type="button" data-filter-action="clear" data-no-auto-close> <span class="nds-label">Reset</span> </button> <button class="nds-btn nds-primary nds-dropmenu-item" type="button" data-filter-action="apply"> <span class="nds-label">Filter</span> </button> </div> </div> </div> </div> </div> </div> <div class="nds-bar-row"> <div class="nds-bar-start"> <div class="nds-filter-applied" data-filter-target="paginatedTableBody" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> </div> </div> <table class="nds-table nds-compact nds-sortable"> <thead> <tr> <th> <div class="nds-col-header"> <span class="nds-label">#</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by row number"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">Service</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by service"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">System</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by system"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th>Popularity</th> </tr> </thead> <tbody id="paginatedTableBody" class="nds-paged-content" style="--per-page: 5;" data-filter-items="tr"> <tr class="nds-page-item"> <td>1</td> <td>Identity Verification</td> <td><span class="nds-tag nds-blue nds-sm"><span class="nds-label" data-filter="system">Identity & Records</span></span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label" data-filter="popularity" data-filter-value="most_used">Most Used</span></span></td> </tr> <tr class="nds-page-item"> <td>2</td> <td>Passport Renewal</td> <td><span class="nds-tag nds-blue nds-sm"><span class="nds-label" data-filter="system">Identity & Records</span></span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label" data-filter="popularity" data-filter-value="most_used">Most Used</span></span></td> </tr> <tr class="nds-page-item"> <td>3</td> <td>Birth Certificate Request</td> <td><span class="nds-tag nds-blue nds-sm"><span class="nds-label" data-filter="system">Identity & Records</span></span></td> <td><span class="nds-tag nds-sm" data-status="neutral"><span class="nds-label" data-filter="popularity" data-filter-value="standard">Standard</span></span></td> </tr> <tr class="nds-page-item"> <td>4</td> <td>Marriage Contract Registration</td> <td><span class="nds-tag nds-blue nds-sm"><span class="nds-label" data-filter="system">Identity & Records</span></span></td> <td><span class="nds-tag nds-sm" data-status="neutral"><span class="nds-label" data-filter="popularity" data-filter-value="standard">Standard</span></span></td> </tr> <tr class="nds-page-item"> <td>5</td> <td>Driver's License Services</td> <td><span class="nds-tag nds-blue nds-sm"><span class="nds-label" data-filter="system">Transport & Vehicles</span></span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label" data-filter="popularity" data-filter-value="most_used">Most Used</span></span></td> </tr> </tbody> </table> </div> <!-- Auto-pagination nav bound to the content wrapper by id --> <nav class="nds-pagination" data-auto-pagination="paginatedTableBody" aria-label="Table pagination"></nav>

Expandable Sub-Rows

A detail row that opens beneath its parent, holding anything from a paragraph to a nested table. Reach for this when a record has supporting data that does not deserve its own page

Static sub-rows
Reference Service Amount Actions
TXN-2026-001 Commercial registration 1,200
TXN-2026-002 Work permit renewal 650

Opens on first paint. Leave hidden off the sub row and ship aria-expanded="true" plus data-state="open" on the toggle. Add colspan too: it is corrected at init either way, but a row that is already visible would reflow when that happens.

TXN-2026-003 Municipal licence 2,400
<table class="nds-table" id="subRowDemo"> <thead> <tr> <th>Reference</th> <th>Service</th> <th>Amount</th> <th class="actions-column">Actions</th> </tr> </thead> <tbody> <tr> <td>TXN-2026-001</td> <td>Commercial registration</td> <td>1,200</td> <td class="actions-column"> <button class="nds-btn nds-subtle nds-md nds-menu-btn" data-sub-toggle aria-controls="sub-row-1" aria-expanded="false"> <span class="nds-label">Details</span> </button> </td> </tr> <tr id="sub-row-1" class="nds-sub" hidden> <td> <table class="nds-table nds-compact"> <thead> <tr> <th>Line item</th> <th>Amount</th> </tr> </thead> <tbody> <tr> <td>Processing fee</td> <td>25</td> </tr> <tr> <td>Service charge</td> <td>1,020</td> </tr> <tr> <td>VAT (15%)</td> <td>155</td> </tr> </tbody> </table> </td> </tr> <tr> <td>TXN-2026-002</td> <td>Work permit renewal</td> <td>650</td> <td class="actions-column"> <button class="nds-btn nds-subtle nds-md nds-menu-btn" data-sub-toggle aria-controls="sub-row-2" aria-expanded="true" data-state="open"> <span class="nds-label">Details</span> </button> </td> </tr> <tr id="sub-row-2" class="nds-sub"> <td colspan="4"> <p>Opens on first paint.</p> </td> </tr> </tbody> </table>

Built-in Features

Auto-initialization

Every .nds-table on the page is automatically wrapped in a responsive scroll container. Opt into gradient fade masks with nds-mask.

Column Sorting

Columns cycle through ascending, descending, and original order. Numbers, dates, and text are detected and sorted appropriately.

Row Selection

Header checkbox toggles all rows with indeterminate state support. Selected rows receive a distinct background highlight that persists across striped rows.

Column Visibility

Point a Dropmenu at a table with data-columns-target and it becomes a checklist of that table's columns, built from the <thead> on first open. Hidden columns are skipped by exports, the trigger label gains a count, and the choice is remembered for tables that have an id.

Scroll Awareness

Add nds-mask to fade the overflow edges with a gradient that updates as the user scrolls to indicate more content in either direction. The mask clips descendants to the wrapper, so avoid it on tables with dropmenus, tooltips, or other overflowing popovers.

Keyboard Accessible

Sort headers are focusable buttons that respond to Enter and Space. Interactive elements within cells receive visible focus rings.

Row Highlighting

Alternating row backgrounds make large datasets easier to scan. Add nds-interactive to opt into hover highlighting on tables whose rows respond to clicks or selection.

Loading State

Add data-state="loading" to show a shimmer animation across all cells while data is being fetched.

Programmatic Control

Access sort state, reset sorting, and reinitialize tables after dynamic content changes through the NDS.Tables namespace.

Expandable Sub-Rows

Put data-sub-toggle anywhere in a row and a detail row opens beneath it, holding a nested table, a form, or plain content. Sorting carries each sub with its parent, filtering and pagination hide it alongside, and collapsing keeps the DOM so anything typed inside survives.

Lazy Detail Loading

A toggle with no detail row yet fires nds:table:sub-request and spins while you fetch. Answer with setContent(html).open() and the result is cached, so reopening never asks twice.

Row Export

Export selected rows (or all rows, across paginated pages) to CSV, Excel, or PDF via the NDS.Export namespace. Arabic UTF-8 and RTL print are handled natively; zero runtime dependencies.

Usage Guidelines

Best Practices

  • Use tables for structured, comparable data where users need to scan across rows and columns. For simple key-value pairs, use a Definition List instead
  • Do not use tables for page layout or displaying Cards in a grid. Use the Grid layout for that
  • Choose compact tables for dense administrative data (logs, inventories, audit trails) and standard tables when rows contain rich content like tags, avatars, or action buttons
  • Enable sorting only on columns with meaningful sort order. Status columns with tags are poor candidates for sorting
  • Add row selection when the interface supports bulk operations (delete, export, assign). Pair the table with an action bar that appears when rows are selected
  • Apply nds-interactive only when rows actually do something on click or hover (open a detail panel, link to a record, toggle selection). Leave it off for read-only data so the hover highlight does not suggest interactivity that is not there
  • Offer a column-visibility menu on wide tables so users can trim a horizontally-scrolling table to the columns they care about. Lock the identifier column with data-columns-lock so a row can never lose its label, and give the table an id so the choice survives a reload
  • Set --max-width when placing a table in a narrow container or side panel to trigger the responsive scroll wrapper early
  • Reach for sub-rows when a record has supporting detail that would bloat the table as extra columns: line items, attached files, an audit trail. If the detail is a full record in its own right, link to a page instead
  • Load sub-row content lazily when it is expensive or rarely opened. Ship it inline when it is small and most users will expand it, since inline content needs no JavaScript at all
  • Use pagination for datasets over 15-20 rows. Showing too many rows slows rendering and makes scanning harder
  • Keep header labels short and descriptive. Avoid abbreviations that require explanation
  • Place the most important identifier column (name, ID, title) first. Put action buttons or status indicators in the last column
  • Add a <caption> element for screen readers when the table's purpose is not clear from surrounding headings

Modifier Classes

ClassDescription
nds-compactReduces row height to 48px. Override with --table-row-height for custom values
nds-responsiveMarks the table for automatic wrapping in a horizontal-scroll container (.nds-table-wrapper). Pair with --max-width / --min-width to tune the scroll breakpoint
nds-maskApplies gradient fade masks on the overflow edges when the table scrolls horizontally. Off by default. Note: mask clips descendants to the wrapper and breaks overflowing UI like dropmenus, tooltips, and popovers that escape table bounds
nds-interactiveEnables hover background changes on rows so the table reads as clickable. Off by default to keep static data tables from suggesting interactivity
nds-sortableEnables column sorting. Use nds-col-header with nds-sort-btn nds-icon-only inside sortable <th> elements
nds-centerCenter-aligns all cell content across the table
nds-col-headerFlex container inside <th> that holds the label and actions side by side
nds-col-actionsContainer for action buttons (sort, filter, etc.) inside a column header
nds-sort-btn nds-icon-onlySort button class inside column headers that triggers column sorting
nds-page-itemApplied to <tr> elements for client-side pagination (used with nds-paged-content). Put it on parent rows only, never on a nds-sub
nds-subMarks a <tr> as a detail row belonging to the row directly above it. Place it immediately after its parent with a single <td>, and add hidden to start collapsed. One sub per parent row. colspan is managed for you: it is re-derived from the parent's column count at init and again on every toggle, so a stale or missing value never leaves a detail row spanning the wrong width, and a table that gains or loses a column corrects itself. Author it anyway on a sub that starts open, since that row is already visible when init runs and would otherwise reflow
table-actionsFlex container for grouping action buttons within a cell
actions-columnShrinks column to fit content width, preventing unnecessary whitespace
checkbox-columnFallback for browsers without :has() support. Apply to <th> and <td> containing checkboxes to fix column width

Data Attributes

AttributeDescription
data-state="sorted-asc"Set on <th> to mark the initial sort column as ascending
data-state="sorted-desc"Set on <th> to mark the initial sort column as descending
data-state="selected"Set on <tr> to visually highlight a selected row. JS toggles this automatically when checkboxes change
data-state="loading"Set on <table> to show the loading shimmer animation across all cells
data-align="center|start|end"Set on a <th> to align that column: the header and every cell below it, including rows added later by sorting, filtering, or pagination
data-sort-valueSet on <td> to supply an alternate value used for sorting only, when the displayed text would sort incorrectly (e.g. "Free" in a numeric column, a localized date in a text column). The cell still renders its normal content; only the sort order is affected. Not read by Export: use data-export-value for that.
nds-loading (class on <tbody>)Alternate loading trigger: add the nds-loading class directly to <tbody> to shimmer only the body rows while keeping the header visible. Used internally by the Filter and Pagination components during data refresh.
data-auto-pagination="id"Set on <nav class="nds-pagination"> to auto-paginate the nds-paged-content wrapper with that id (omit the value to bind the preceding wrapper)
data-columns-target="id"Set on a nds-dropmenu to turn it into a column-visibility menu for the table with that id. The trigger's icon gains a .nds-badge counting the hidden columns, and a "Reset" button sits under the checklist while any column is hidden
data-columns-listSet on the <fieldset> inside that menu. The checklist is generated into it from the table's <thead>. Author your own rows to opt out of generation
data-columns-lockSet on a <th> to keep that column off the menu so it can never be hidden. The row-selection column is excluded automatically
data-sub-toggleSet on any button inside a row to make it open and close that row's nds-sub. Point aria-controls at the sub row's id. Several toggles may drive one sub (a chevron in the row plus a collapse button inside the content) and they stay in sync. Add nds-menu-btn for a chevron that rotates on its own
data-state="always-open"Set on <table> to let several sub-rows stay open at once. Without it, opening one closes the rest. Pre-rendered open rows are never normalized at load, so multiple open subs paint as authored until the first click
data-filter-items="tr"Set on the <tbody> to mark what Filter treats as an item. On a table it needs no guard against sub-rows: only the tbody's own rows count, so a nested table inside an expanded sub is left alone and detail rows are never filtered in their own right. Subs follow their parent through the cascade
id (on <table>)A table with an id remembers its hidden columns across visits, stored under localStorage['nds-cols-{id}']. The saved set is discarded if the table's column count changes, so a later deploy can add or drop a column without hiding the wrong one

CSS Custom Properties

PropertyDefaultDescription
--table-row-height64px (48px in compact)Row height for data cells. Set on the table or use nds-compact for the 48px preset
--table-cell-padding-blockvar(--spacing-md)Vertical padding inside header and data cells
--table-cell-padding-inlinevar(--spacing-xl)Horizontal padding inside header and data cells. Also sets the inset of the row separator
--max-width100%Maximum width of the responsive scroll wrapper
--min-widthauto-calculatedMinimum width of the table inside the wrapper. Prevents cells from shrinking below content width
--mask-fade-distance48pxWidth of the gradient fade mask on scroll edges
--table-sub-paddingvar(--spacing-xl)Even inset inside a sub-row's cell. Separate from the cell padding knobs: a detail row holds a panel, not a line of text
--table-sub-backgroundvar(--background-surface-sunken)Background of a sub-row's cell, sunk behind the parent rows. Overriding this takes over both light and dark
--per-page10Number of rows shown per page when using pagination (set on nds-paged-content)

JavaScript API

The NDS.Tables API provides methods to initialize, sort, and manage table instances. All tables auto-initialize on page load. Call NDS.Tables.reinit() after dynamically adding new tables to the DOM.

// ── Namespace methods ──────────────────────────────── NDS.Tables.init(); // Initialize all tables on the page NDS.Tables.reinit(); // Re-initialize after dynamic content changes NDS.Tables.recheckWidths(); // Recheck responsive wrappers (e.g. after resize) // ── Utility ────────────────────────────────────────── // Read the sort-safe text of a cell (prefers direct text nodes so // nested sort buttons, icons, and badges don't pollute the value) NDS.Tables.getCellText(tdElement); // Returns string // ── Manual creation ────────────────────────────────── // Create a sortable/selectable controller for a specific table const table = document.querySelector('#myTable'); const instance = NDS.Tables.create(table); // Create a responsive wrapper for a specific table const responsive = NDS.Tables.createResponsive(table); // ── Column visibility ──────────────────────────────── // Show or hide a column by its index in <thead>. Sets [hidden] on the // <th> and on that cell in every row, and skips the column in exports. NDS.Tables.setColumnHidden(table, 3, true); // hide the 4th column NDS.Tables.setColumnHidden(table, 3, false); // show it again // Wire a [data-columns-target] menu manually (auto-wired on init) NDS.Tables.createColumnToggle(document.querySelector('[data-columns-target]')); // setColumnHidden does not persist — only the menu writes the saved set. // Clear a table's remembered columns: localStorage.removeItem('nds-cols-' + table.id); // ── Instance methods (sortable tables) ─────────────── instance.getSortColumn(); // Returns current sort column index (-1 if none) instance.getSortDirection(); // Returns 'asc', 'desc', or null instance.resetSort(); // Clear sorting, restore original row order instance.destroy(); // Remove all event listeners and clean up // ── Instance methods (responsive wrapper) ──────────── responsive.recheckWidth(); // Recheck if scroll is needed responsive.destroy(); // Remove wrapper event listeners // ── Custom events ──────────────────────────────────── // Fires when a column is sorted table.addEventListener('nds:table:sort', (e) => { e.detail.columnIndex; // Sorted column index e.detail.direction; // 'asc', 'desc', or null (reset) e.detail.table; // The <table> element e.detail.button; // The active sort button (null on reset) }); // Fires when row selection changes table.addEventListener('nds:table:selection', (e) => { e.detail.selectedCount; // Number of selected rows e.detail.totalCount; // Total number of selectable rows e.detail.selectedRows; // Array of selected <tr> elements e.detail.selectedIndexes; // Array of selected row indexes e.detail.table; // The <table> element }); // Fires when a column is shown or hidden table.addEventListener('nds:table:columns', (e) => { e.detail.index; // Column index in <thead> e.detail.hidden; // true when the column was hidden e.detail.table; // The <table> element e.detail.restored; // true when replayed from storage at init, not chosen by the user }); // ── Sub-rows ───────────────────────────────────────── // row() returns a handle for one row. Pass the parent row or its sub: // a control inside the detail content uses the same call as the toggle above. const row = document.querySelector('#myTable tbody tr'); NDS.Tables.row(row).sub.setContent('<p>Details</p>'); // build or replace the content NDS.Tables.row(row).sub.open(); // show it (single-open unless data-state="always-open") NDS.Tables.row(row).sub.close(); // hide it NDS.Tables.row(row).sub.toggle(); // flip it NDS.Tables.row(row).sub.el; // the <tr class="nds-sub">, or null // Content and visibility are separate, and the DOM is never torn down: // values typed into a form inside a sub survive a collapse. setContent // returns the sub handle, so the lazy case reads as one line. // ── Lazy loading ───────────────────────────────────── // Clicking a toggle whose row has no sub yet fires sub-request and spins the // toggle. NDS never fetches: you do, then hand back the markup. detail.signal // aborts the request if the user cancels by clicking the spinning toggle. table.addEventListener('nds:table:sub-request', (e) => { const { row, signal } = e.detail; NDS.request(`/transactions/${row.dataset.id}/details`, { signal }) .then(({ data }) => NDS.Tables.row(row).sub.setContent(data).open()) .catch(err => { // Aborted means the user cancelled: NDS already released the toggle. // Any other failure must call close(), or the toggle keeps spinning. if (err.name !== 'AbortError') NDS.Tables.row(row).sub.close(); }); }); // Fires when a sub-row opens. Late-init any widgets inside detail.sub table.addEventListener('nds:table:sub-open', (e) => { e.detail.row; // The parent <tr> e.detail.sub; // The <tr class="nds-sub"> now visible e.detail.table; // The <table> element NDS.CustomSelect?.reinit(e.detail.sub); }); // Fires when a sub-row closes table.addEventListener('nds:table:sub-close', (e) => { e.detail.row; // The parent <tr> e.detail.sub; // The <tr class="nds-sub"> now hidden e.detail.table; // The <table> element });
Last Modified Date: 29/07/2026 - 04:31 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks