Standard Table
The default table layout with striped rows and rounded borders. Toggle Interactive to preview the optional hover state
| Name | 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 |
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 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 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 | 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 |
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 |
| 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 |
Loading State
A shimmer animation on table cells indicates data is being fetched
| Name | 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 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.
|
#
|
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 |
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
| 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 | ||||||
|
Opening one sub closes the others. Add data-state="always-open" to the table to let several stay open at once. |
||||||||
Built-in Features
Every .nds-table on the page is automatically wrapped in a responsive scroll container. Opt into gradient fade masks with nds-mask.
Columns cycle through ascending, descending, and original order. Numbers, dates, and text are detected and sorted appropriately.
Header checkbox toggles all rows with indeterminate state support. Selected rows receive a distinct background highlight that persists across striped rows.
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.
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.
Sort headers are focusable buttons that respond to Enter and Space. Interactive elements within cells receive visible focus rings.
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.
Add data-state="loading" to show a shimmer animation across all cells while data is being fetched.
Access sort state, reset sorting, and reinitialize tables after dynamic content changes through the NDS.Tables namespace.
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.
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.
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-interactiveonly 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-lockso a row can never lose its label, and give the table anidso the choice survives a reload - Set
--max-widthwhen 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
| Class | Description |
|---|---|
nds-compact | Reduces row height to 48px. Override with --table-row-height for custom values |
nds-responsive | Marks 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-mask | Applies 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-interactive | Enables hover background changes on rows so the table reads as clickable. Off by default to keep static data tables from suggesting interactivity |
nds-sortable | Enables column sorting. Use nds-col-header with nds-sort-btn nds-icon-only inside sortable <th> elements |
nds-center | Center-aligns all cell content across the table |
nds-col-header | Flex container inside <th> that holds the label and actions side by side |
nds-col-actions | Container for action buttons (sort, filter, etc.) inside a column header |
nds-sort-btn nds-icon-only | Sort button class inside column headers that triggers column sorting |
nds-page-item | Applied to <tr> elements for client-side pagination (used with nds-paged-content). Put it on parent rows only, never on a nds-sub |
nds-sub | Marks 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-actions | Flex container for grouping action buttons within a cell |
actions-column | Shrinks column to fit content width, preventing unnecessary whitespace |
checkbox-column | Fallback for browsers without :has() support. Apply to <th> and <td> containing checkboxes to fix column width |
Data Attributes
| Attribute | Description |
|---|---|
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-value | Set 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-list | Set 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-lock | Set 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-toggle | Set 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
| Property | Default | Description |
|---|---|---|
--table-row-height | 64px (48px in compact) | Row height for data cells. Set on the table or use nds-compact for the 48px preset |
--table-cell-padding-block | var(--spacing-md) | Vertical padding inside header and data cells |
--table-cell-padding-inline | var(--spacing-xl) | Horizontal padding inside header and data cells. Also sets the inset of the row separator |
--max-width | 100% | Maximum width of the responsive scroll wrapper |
--min-width | auto-calculated | Minimum width of the table inside the wrapper. Prevents cells from shrinking below content width |
--mask-fade-distance | 48px | Width of the gradient fade mask on scroll edges |
--table-sub-padding | var(--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-background | var(--background-surface-sunken) | Background of a sub-row's cell, sunk behind the parent rows. Overriding this takes over both light and dark |
--per-page | 10 | Number 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.