Table
A template-driven table. Unlike bk-grid, which renders cells from a column config, bk-table renders none of your markup, you write the <thead>/<tbody> and it supplies the behaviour: sort and filter state, pagination, sticky header and columns, virtual scrolling.
Preview
A working preview of Table in the Brickclay style. Try it.
| Order No | Customer | Amount | Status | Created |
|---|---|---|---|---|
| #1042 | Northwind | $12,400 | Paid | 2026-10-02 |
| #1043 | Contoso | $3,180 | Pending | 2026-10-05 |
| #1044 | Fabrikam | $8,900 | Paid | 2026-10-11 |
| #1045 | Tailspin | $1,250 | Overdue | 2026-10-14 |
| #1046 | Wingtip | $22,070 | Paid | 2026-10-20 |
| Total | $47,800 | |||
That inversion is the point. Column spans, grouped headers, editable cells, nested tables and summary rows are all just markup here, and none of them can be expressed as a config object without inventing a language for it. Styling, sizing and every input control are the same bk components used everywhere else.
Playground
Every switch below maps to one input. The markup underneath is generated from the current settings, so it is the thing to copy rather than an illustration of it.
Turn stableColumnWidth off, then sort or filter: the columns resize to whatever happens to be on screen. That is the default table behaviour the input exists to stop.
| Name | Age | Department | Address |
|---|---|---|---|
John Brown | 24 | Engineering | 10 Lake Park, London No. 1 |
Jim Green | 31 | Design | 11 Lake Park, London No. 2 |
Joe Black | 38 | Finance | 12 Lake Park, London No. 3 |
Jane Cooper | 45 | Operations | 13 Lake Park, London No. 4 |
Cody Fisher | 52 | Engineering | 14 Lake Park, London No. 5 |
Esther Howard | 59 | Design | 15 Lake Park, London No. 6 |
Kristin Watson | 30 | Finance | 16 Lake Park, London No. 7 |
Cameron Williamson | 37 | Operations | 17 Lake Park, London No. 8 |
Brooklyn Simmons | 44 | Engineering | 18 Lake Park, London No. 9 |
Leslie Alexander | 51 | Design | 19 Lake Park, London No. 10 |
John Brown | 58 | Finance | 20 Lake Park, London No. 11 |
Jim Green | 29 | Operations | 21 Lake Park, London No. 12 |
Joe Black | 36 | Engineering | 22 Lake Park, London No. 13 |
Jane Cooper | 43 | Design | 23 Lake Park, London No. 14 |
Cody Fisher | 50 | Finance | 24 Lake Park, London No. 15 |
Esther Howard | 57 | Operations | 25 Lake Park, London No. 16 |
Kristin Watson | 28 | Engineering | 26 Lake Park, London No. 17 |
Cameron Williamson | 35 | Design | 27 Lake Park, London No. 18 |
Brooklyn Simmons | 42 | Finance | 28 Lake Park, London No. 19 |
Leslie Alexander | 49 | Operations | 29 Lake Park, London No. 20 |
John Brown | 56 | Engineering | 30 Lake Park, London No. 21 |
Jim Green | 27 | Design | 31 Lake Park, London No. 22 |
Joe Black | 34 | Finance | 32 Lake Park, London No. 23 |
Rows per page
Showing 1 to 23 of 23 Records
1 - 1 of 1
-
- 1
<bk-table #t="bkTable" [data]="rows" [bordered]="true"> <thead> <tr> <th bk-th [sortFn]="byName" [left]="pinned">Name</th> <th bk-th [sortFn]="byAge">Age</th> <th bk-th [filters]="depts" [filterFn]="byDept">Department</th> <th bk-th >Address</th> </tr> </thead> <tbody> @for (row of t.renderData(); track row.id) { <tr> … </tr> } </tbody> </bk-table>
1 · Simple table with actions
Actions are ordinary buttons in an ordinary cell, no action registry, no permission callbacks to configure. Show, hide and disable them with the template syntax you already use.
| Name | Age | Department | Address | Action |
|---|---|---|---|---|
John Brown | 24 | Engineering | 10 Lake Park, London No. 1 | EditDelete |
Jim Green | 31 | Design | 11 Lake Park, London No. 2 | EditDelete |
Joe Black | 38 | Finance | 12 Lake Park, London No. 3 | EditDelete |
Jane Cooper | 45 | Operations | 13 Lake Park, London No. 4 | EditDelete |
Cody Fisher | 52 | Engineering | 14 Lake Park, London No. 5 | EditDelete |
Esther Howard | 59 | Design | 15 Lake Park, London No. 6 | EditDelete |
2 · Row selection
A checkbox column, select-all with an indeterminate state, and custom entries in the select-all dropdown (ng-zorro's nzSelections). Header state is derived from the rows rather than stored, so it can't drift out of sync with them.
| Name | Age | Department | Status | |
|---|---|---|---|---|
| John Brown | 24 | Engineering | Inactive | |
| Jim Green | 31 | Design | Active | |
| Joe Black | 38 | Finance | Active | |
| Jane Cooper | 45 | Operations | Inactive | |
| Cody Fisher | 52 | Engineering | Active | |
| Esther Howard | 59 | Design | Active | |
| Kristin Watson | 30 | Finance | Inactive | |
| Cameron Williamson | 37 | Operations | Active |
3–5 · Sorting, filtering, multi-sort and reset
Pass sortFn a comparator and the table sorts in-process; pass it true and the column is still sortable but only reports the change through queryParams for the server to act on. filterFn works the same way. Age and Department both carry a sortPriority, so sorting both applies Age first and Department only to the rows Age ties, the badge beside each arrow shows the rank.
| Name | Age | Department | Status |
|---|---|---|---|
John Brown | 24 | Engineering | Inactive |
Jim Green | 31 | Design | Active |
Joe Black | 38 | Finance | Active |
Jane Cooper | 45 | Operations | Inactive |
Cody Fisher | 52 | Engineering | Active |
Esther Howard | 59 | Design | Active |
Showing 1 to 6 of 12 Records
1 - 2 of 2
- 2
Status filters in single-select mode (radios); Department allows multiple (checkboxes).
Server-side mode
Set frontPagination to false, feed total, and mark columns [sortFn]="true". The table stops touching the rows and reports everything the server needs through queryParams.
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
Showing 1 to 4 of 120 Records
1 - 2 of 30
frontPagination: front vs. server
Same simulated 60-row "server" all three ways. front: the request ignores paging and the server returns everything; the table slices it, the only correct use of frontPagination: true. server: the request is paged, frontPagination is false, one page plus the real total comes back, and the table trusts both instead of computing either. front (misused) fetches exactly like server, same one page, same real total available, but leaves frontPagination on true. That's the whole bug regions.ts had: the table has no way to know 10 rows isn't the whole dataset, so it paginates over just that page and the real total never gets used.
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
Cody Fisher | 52 | Engineering |
Esther Howard | 59 | Design |
Kristin Watson | 30 | Finance |
Cameron Williamson | 37 | Operations |
Brooklyn Simmons | 44 | Engineering |
Leslie Alexander | 51 | Design |
Showing 1 to 10 of 60 Records
1 - 2 of 6
Server actually has 60 rows. This fetch handed the table 60 of them. Table's pager is showing a total of 60, correct.
6 · Sizes
Four densities, roomiest first: lg, default, md, sm. Each moves the type scale along with the padding, padding alone makes a large table look like a normal one that has been stretched. sm is the one for a modal, where the surrounding chrome has already spent most of the height. middle and small still work as ng-zorro's names for md and sm.
size = "lg"
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
size = "default"
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
size = "md"
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
size = "sm"
| Name | Age | Department |
|---|---|---|
John Brown | 24 | Engineering |
Jim Green | 31 | Design |
Joe Black | 38 | Finance |
Jane Cooper | 45 | Operations |
8 · Expandable rows
For rows carrying more than a row can show. The detail row is hidden rather than destroyed, so anything with state inside it survives being collapsed. The second table passes a custom template to expandIcon.
| Name | Age | Department | |
|---|---|---|---|
| John Brown | 24 | Engineering | |
| John Brown, 10 Lake Park, London No. 1. Status Inactive. Even-numbered rows use the custom + / − expand icon. | |||
| + | Jim Green | 31 | Design |
| Jim Green, 11 Lake Park, London No. 2. Status Active. Even-numbered rows use the custom + / − expand icon. | |||
| Joe Black | 38 | Finance | |
| Joe Black, 12 Lake Park, London No. 3. Status Active. Even-numbered rows use the custom + / − expand icon. | |||
| + | Jane Cooper | 45 | Operations |
| Jane Cooper, 13 Lake Park, London No. 4. Status Inactive. Even-numbered rows use the custom + / − expand icon. | |||
| Cody Fisher | 52 | Engineering | |
| Cody Fisher, 14 Lake Park, London No. 5. Status Active. Even-numbered rows use the custom + / − expand icon. |
9 · colSpan and rowSpan
The native colspan and rowspan attributes, used directly. There is no wrapper input for them because there doesn't need to be, this is the clearest case for why the control projects your markup instead of generating it.
| Name | Age | Department | Address |
|---|---|---|---|
John Brown | Shared cohort (rows 1–3) | Engineering | 10 Lake Park, London No. 1 |
Jim Green | Design | 11 Lake Park, London No. 2 | |
Joe Black | Finance | 12 Lake Park, London No. 3 | |
Jane Cooper | 45 | Department and address merged for this row | |
Cody Fisher | 52 | Engineering | 14 Lake Park, London No. 5 |
10 · Tree structure
A table renders a list, so the tree is flattened to one, only branches whose ancestors are all open contribute rows. indentSize sets the pixels per level; leaf rows get a spacer where the toggle would be so their labels stay aligned.
| Name | Role | Headcount |
|---|---|---|
Product | Division | 42 |
Design | Team | 12 |
Engineering | Team | 30 |
Finance | Division | 8 |
10b · Dragging children and sub-children
The same tree, draggable at every depth. The pointer's position within the row under it decides the relationship, and an indicator shows it before you commit: a line in the top third inserts before that row as a sibling, the middle third tints the row and drops inside it as a child, the bottom third inserts after it. One gesture covers reordering, re-parenting and nesting, no modes.
Rest the pointer on a collapsed branch's middle for a moment and it opens, so you can drop deeper without cancelling. Illegal targets, the dragged row itself, anything inside its own subtree, simply show no indicator: try dragging a division onto one of its own teams. A node's subtree always travels with it.
| Name | Role | Headcount | |
|---|---|---|---|
Product | Division | 42 | |
Design | Team | 12 | |
| Esther Howard | Lead Designer | 1 | |
| Cody Fisher | Designer | 1 | |
Engineering | Team | 30 | |
| Jim Green | Staff Engineer | 1 | |
| Joe Black | Engineer | 1 | |
Finance | Division | 8 | |
| Kristin Watson | Controller | 1 |
11 · Fixed header and fixed columns
scroll.y caps the body height and pins the header; scroll.x sets a minimum width so columns scroll instead of collapsing. Pin a column with [left]="true" or [right]="true", the offset is measured from the rendered columns, so it stays right when widths change. Pinned columns must be contiguous from their edge.
| Name | Age | Department | Address | Status | Department | Address | Action |
|---|---|---|---|---|---|---|---|
John Brown | 24 | Engineering | 10 Lake Park, London No. 1 | Inactive | Engineering | 10 Lake Park, London No. 1 | Edit |
Jim Green | 31 | Design | 11 Lake Park, London No. 2 | Active | Design | 11 Lake Park, London No. 2 | Edit |
Joe Black | 38 | Finance | 12 Lake Park, London No. 3 | Active | Finance | 12 Lake Park, London No. 3 | Edit |
Jane Cooper | 45 | Operations | 13 Lake Park, London No. 4 | Inactive | Operations | 13 Lake Park, London No. 4 | Edit |
Cody Fisher | 52 | Engineering | 14 Lake Park, London No. 5 | Active | Engineering | 14 Lake Park, London No. 5 | Edit |
Esther Howard | 59 | Design | 15 Lake Park, London No. 6 | Active | Design | 15 Lake Park, London No. 6 | Edit |
Kristin Watson | 30 | Finance | 16 Lake Park, London No. 7 | Inactive | Finance | 16 Lake Park, London No. 7 | Edit |
Cameron Williamson | 37 | Operations | 17 Lake Park, London No. 8 | Active | Operations | 17 Lake Park, London No. 8 | Edit |
Brooklyn Simmons | 44 | Engineering | 18 Lake Park, London No. 9 | Active | Engineering | 18 Lake Park, London No. 9 | Edit |
Leslie Alexander | 51 | Design | 19 Lake Park, London No. 10 | Inactive | Design | 19 Lake Park, London No. 10 | Edit |
John Brown | 58 | Finance | 20 Lake Park, London No. 11 | Active | Finance | 20 Lake Park, London No. 11 | Edit |
Jim Green | 29 | Operations | 21 Lake Park, London No. 12 | Active | Operations | 21 Lake Park, London No. 12 | Edit |
Joe Black | 36 | Engineering | 22 Lake Park, London No. 13 | Inactive | Engineering | 22 Lake Park, London No. 13 | Edit |
Jane Cooper | 43 | Design | 23 Lake Park, London No. 14 | Active | Design | 23 Lake Park, London No. 14 | Edit |
Cody Fisher | 50 | Finance | 24 Lake Park, London No. 15 | Active | Finance | 24 Lake Park, London No. 15 | Edit |
Esther Howard | 57 | Operations | 25 Lake Park, London No. 16 | Inactive | Operations | 25 Lake Park, London No. 16 | Edit |
Kristin Watson | 28 | Engineering | 26 Lake Park, London No. 17 | Active | Engineering | 26 Lake Park, London No. 17 | Edit |
Cameron Williamson | 35 | Design | 27 Lake Park, London No. 18 | Active | Design | 27 Lake Park, London No. 18 | Edit |
Brooklyn Simmons | 42 | Finance | 28 Lake Park, London No. 19 | Inactive | Finance | 28 Lake Park, London No. 19 | Edit |
Leslie Alexander | 49 | Operations | 29 Lake Park, London No. 20 | Active | Operations | 29 Lake Park, London No. 20 | Edit |
John Brown | 56 | Engineering | 30 Lake Park, London No. 21 | Active | Engineering | 30 Lake Park, London No. 21 | Edit |
Jim Green | 27 | Design | 31 Lake Park, London No. 22 | Inactive | Design | 31 Lake Park, London No. 22 | Edit |
Joe Black | 34 | Finance | 32 Lake Park, London No. 23 | Active | Finance | 32 Lake Park, London No. 23 | Edit |
Jane Cooper | 41 | Operations | 33 Lake Park, London No. 24 | Active | Operations | 33 Lake Park, London No. 24 | Edit |
Cody Fisher | 48 | Engineering | 34 Lake Park, London No. 25 | Inactive | Engineering | 34 Lake Park, London No. 25 | Edit |
Esther Howard | 55 | Design | 35 Lake Park, London No. 26 | Active | Design | 35 Lake Park, London No. 26 | Edit |
Kristin Watson | 26 | Finance | 36 Lake Park, London No. 27 | Active | Finance | 36 Lake Park, London No. 27 | Edit |
Cameron Williamson | 33 | Operations | 37 Lake Park, London No. 28 | Inactive | Operations | 37 Lake Park, London No. 28 | Edit |
Brooklyn Simmons | 40 | Engineering | 38 Lake Park, London No. 29 | Active | Engineering | 38 Lake Park, London No. 29 | Edit |
Leslie Alexander | 47 | Design | 39 Lake Park, London No. 30 | Active | Design | 39 Lake Park, London No. 30 | Edit |
ellipsis on Name keeps every row one line, a wrapped row is taller than its neighbours, and that unevenness is exactly what makes a capped scroll.y look broken at the boundary, not the cap itself. Height and width both already accept any CSS length (vh, %, calc()), so scroll: { y: 'calc(100vh - 320px)' } is valid today; there's no built-in "fill remaining space" mode beyond that.
12 · Grouping table head
Two header rows, spanned with the native attributes. The second row sticks below the first, so both stay visible when the body scrolls. [right]="true" on Work, Department and Status pins them as one contiguous block from the right edge, safe here because nothing spans down from row 1 on that side. Pinning Age next to the rowspan'd Name on the left is the case that doesn't work yet. scroll is a plain input, so breakpoint-based sizing (768px / 1024px, below) is computed in the component and passed in, the same way you'd do it for any other value, resize the window to see it switch.
| Name | Personal | Work |
|---|---|---|
Age | Address | Department | Status |
John Brown | 24 | 10 Lake Park, London No. 1 | Engineering | Inactive |
Jim Green | 31 | 11 Lake Park, London No. 2 | Design | Active |
Joe Black | 38 | 12 Lake Park, London No. 3 | Finance | Active |
Jane Cooper | 45 | 13 Lake Park, London No. 4 | Operations | Inactive |
Cody Fisher | 52 | 14 Lake Park, London No. 5 | Engineering | Active |
private readonly width = toSignal(
fromEvent(window, 'resize').pipe(map(() => window.innerWidth), startWith(window.innerWidth))
);
readonly tableScroll = computed<BkTableScroll>(() => {
const w = this.width() ?? window.innerWidth;
if (w < 768) return { x: '600px', y: '220px' }; // mobile
if (w < 1024) return { x: '900px', y: '240px' }; // tablet
return { x: '1600px', y: '260px' }; // desktop
});13 · Editable cells
Click a Name or Address cell to edit it. This needs no table API at all, it is an @if swapping text for a bk-input inside a cell you already own.
| Name | Address | Age |
|---|---|---|
John Brown | 10 Lake Park, London No. 1 | 24 |
Jim Green | 11 Lake Park, London No. 2 | 31 |
Joe Black | 12 Lake Park, London No. 3 | 38 |
Jane Cooper | 13 Lake Park, London No. 4 | 45 |
Cody Fisher | 14 Lake Park, London No. 5 | 52 |
14 · Editable rows
The whole row swaps into inputs at once. Edits go to a working copy, so Cancel leaves the committed row untouched rather than needing to be undone field by field.
| Name | Age | Address | Action |
|---|---|---|---|
John Brown | 24 | 10 Lake Park, London No. 1 | Edit |
Jim Green | 31 | 11 Lake Park, London No. 2 | Edit |
Joe Black | 38 | 12 Lake Park, London No. 3 | Edit |
Jane Cooper | 45 | 13 Lake Park, London No. 4 | Edit |
Cody Fisher | 52 | 14 Lake Park, London No. 5 | Edit |
15 · Nested tables
A second bk-table inside an expanded row. It is a table like any other, its own sorting, its own size, because the outer one makes no assumptions about what a cell contains.
| Name | Department | Status | |
|---|---|---|---|
| John Brown | Engineering | Inactive | |
| InvoiceDateAmountStatusINV-112026-01-101,201PaidINV-122026-02-112,401PendingINV-132026-03-123,601Pending | |||
| Jim Green | Design | Active | |
| InvoiceDateAmountStatusINV-212026-01-101,202PaidINV-222026-02-112,402PendingINV-232026-03-123,602Pending | |||
| Joe Black | Finance | Active | |
| InvoiceDateAmountStatusINV-312026-01-101,203PaidINV-322026-02-112,403PendingINV-332026-03-123,603Pending | |||
| Jane Cooper | Operations | Inactive | |
| InvoiceDateAmountStatusINV-412026-01-101,204PaidINV-422026-02-112,404PendingINV-432026-03-123,604Pending |
16 · Virtual scroll
10,000 rows, only the visible ones in the DOM. Rows are created by the viewport as it scrolls, so they come from a [bkVirtualScroll] template rather than a projected <tbody>, and virtualItemSize must match the real row height.
Rows have to be uniform height for the viewport to predict scroll position, so virtual scroll does not combine with rowspan, tree data or expandable rows.
| # | Name | Age | Address |
|---|---|---|---|
1 | John Brown | 24 | 10 Lake Park, London No. 1 |
2 | Jim Green | 31 | 11 Lake Park, London No. 2 |
3 | Joe Black | 38 | 12 Lake Park, London No. 3 |
4 | Jane Cooper | 45 | 13 Lake Park, London No. 4 |
5 | Cody Fisher | 52 | 14 Lake Park, London No. 5 |
6 | Esther Howard | 59 | 15 Lake Park, London No. 6 |
7 | Kristin Watson | 30 | 16 Lake Park, London No. 7 |
8 | Cameron Williamson | 37 | 17 Lake Park, London No. 8 |
9 | Brooklyn Simmons | 44 | 18 Lake Park, London No. 9 |
10 | Leslie Alexander | 51 | 19 Lake Park, London No. 10 |
11 | John Brown | 58 | 20 Lake Park, London No. 11 |
12 | Jim Green | 29 | 21 Lake Park, London No. 12 |
13 | Joe Black | 36 | 22 Lake Park, London No. 13 |
14 | Jane Cooper | 43 | 23 Lake Park, London No. 14 |
15 | Cody Fisher | 50 | 24 Lake Park, London No. 15 |
16 | Esther Howard | 57 | 25 Lake Park, London No. 16 |
17 | Kristin Watson | 28 | 26 Lake Park, London No. 17 |
17 · Drag sorting
CDK drag-and-drop with the two table-specific fixes carried over from bk-grid: the drag preview keeps its column widths instead of collapsing, and dragging near an edge scrolls the table so rows below the fold stay reachable. The handle is <bk-drag-handle>, a small component, not a fixed part of the table, so where it sits and what it looks like are both just markup choices.
| Name | Age | Department | |
|---|---|---|---|
| John Brown | 24 | Engineering | |
| Jim Green | 31 | Design | |
| Joe Black | 38 | Finance | |
| Jane Cooper | 45 | Operations | |
| Cody Fisher | 52 | Engineering | |
| Esther Howard | 59 | Design | |
| Kristin Watson | 30 | Finance |
18–19 · Break word and ellipsis
breakWord allows breaks inside a word, so an unbroken code or URL wraps instead of pushing past the cell. ellipsis truncates on one line instead, pair it with a tooltip when the full value still matters.
| Reference (BreakWord) | Note (Ellipsis) | Note (Default Wrap) |
|---|---|---|
ORD-2026-0001-AAAA-BBBB-CCCC-DDDD | Delivered to the loading bay behind the building; the driver waited for a signature. | Delivered to the loading bay behind the building; the driver waited for a signature. |
REF-INTERNAL-LEDGER-ENTRY-9928374-XYZ | Reconciliation pending because the supplier reference does not match our purchase order. | Reconciliation pending because the supplier reference does not match our purchase order. |
20 · Summary row
Totals live in a <tfoot bkTableSummary>. With fixed the footer pins to the bottom of the scroll container, the counterpart to the fixed header, so totals stay visible while the body scrolls.
| Name | Department | Age |
|---|---|---|
John Brown | Engineering | 24 |
Jim Green | Design | 31 |
Joe Black | Finance | 38 |
Jane Cooper | Operations | 45 |
Cody Fisher | Engineering | 52 |
Esther Howard | Design | 59 |
Kristin Watson | Finance | 30 |
Cameron Williamson | Operations | 37 |
Total (8 people) | 316 | |
Average age | 40 |
21 · Close overlays on scroll
Panels from bk-select, bk-popover, bk-menu and the other overlay controls are portaled out of the table, so a scroll would strand them over the wrong row. bk-table closes them whenever it scrolls on either axis, open a select or the popover below, then scroll the body or drag the horizontal bar. It is on by default; [closeOverlaysOnScroll]="false" turns it off.
| Name | Employee | Coach | Notes | Address |
|---|---|---|---|---|
John Brown | Employee | Coach | View | 10 Lake Park, London No. 1 |
Jim Green | Employee | Coach | View | 11 Lake Park, London No. 2 |
Joe Black | Employee | Coach | View | 12 Lake Park, London No. 3 |
Jane Cooper | Employee | Coach | View | 13 Lake Park, London No. 4 |
Cody Fisher | Employee | Coach | View | 14 Lake Park, London No. 5 |
Esther Howard | Employee | Coach | View | 15 Lake Park, London No. 6 |
Kristin Watson | Employee | Coach | View | 16 Lake Park, London No. 7 |
Cameron Williamson | Employee | Coach | View | 17 Lake Park, London No. 8 |
Brooklyn Simmons | Employee | Coach | View | 18 Lake Park, London No. 9 |
Leslie Alexander | Employee | Coach | View | 19 Lake Park, London No. 10 |
John Brown | Employee | Coach | View | 20 Lake Park, London No. 11 |
Jim Green | Employee | Coach | View | 21 Lake Park, London No. 12 |
Joe Black | Employee | Coach | View | 22 Lake Park, London No. 13 |
Jane Cooper | Employee | Coach | View | 23 Lake Park, London No. 14 |
Cody Fisher | Employee | Coach | View | 24 Lake Park, London No. 15 |
Esther Howard | Employee | Coach | View | 25 Lake Park, London No. 16 |
Kristin Watson | Employee | Coach | View | 26 Lake Park, London No. 17 |
Cameron Williamson | Employee | Coach | View | 27 Lake Park, London No. 18 |
Brooklyn Simmons | Employee | Coach | View | 28 Lake Park, London No. 19 |
Leslie Alexander | Employee | Coach | View | 29 Lake Park, London No. 20 |
John Brown | Employee | Coach | View | 30 Lake Park, London No. 21 |
Jim Green | Employee | Coach | View | 31 Lake Park, London No. 22 |
Joe Black | Employee | Coach | View | 32 Lake Park, London No. 23 |
Jane Cooper | Employee | Coach | View | 33 Lake Park, London No. 24 |
Cody Fisher | Employee | Coach | View | 34 Lake Park, London No. 25 |
<bk-table #t="bkTable" [data]="rows" [bordered]="true" [scroll]="{ x: '1100px', y: '260px' }" [showPagination]="false"> <thead> <tr> <th bk-th>Name</th> <th bk-th>Employee</th> <th bk-th>Coach</th> </tr> </thead> <tbody> @for (row of t.renderData(); track row.id) { <tr> <td bk-td>{{ row.name }}</td> <td bk-td [block]="true"> <bk-select [items]="employees" bindLabel="label" bindValue="value" [(ngModel)]="row.employee" /> </td> <td bk-td [block]="true"> <bk-select [items]="coaches" bindLabel="label" bindValue="value" [(ngModel)]="row.coach" /> </td> </tr> } </tbody> </bk-table>
Without a table
The table is just one user of a public directive. Put bkCloseOverlaysOnScroll on any scrolling element and every overlay control inside it closes when it scrolls. The select in the second box sits in a container without it, so it keeps today's behaviour.
With bkCloseOverlaysOnScroll
Plain container
<!-- imports: [BkCloseOverlaysOnScroll, BkSelect] --> <div bkCloseOverlaysOnScroll class="h-40 overflow-auto"> <bk-select [items]="employees" [(ngModel)]="value" /> ... </div> <!-- or switch it on and off --> <div [bkCloseOverlaysOnScroll]="closeOnScroll()">…</div>
API reference
Import the bundle rather than the pieces: imports: [...BK_TABLE]. It carries BkTable, BkTh, BkTd and every directive below.
Inputs
| Input | Type | Default | Notes |
|---|---|---|---|
data | readonly T[] | [] | Full row set. In server mode, only the current page. |
size | 'lg' | 'default' | 'md' | 'sm' | 'default' | Density, roomiest first. sm is the modal size. middle and small are accepted as ng-zorro’s names for md and sm. |
bordered | boolean | false | Vertical rules between columns. |
title / footer | string | '' | Plain-text bars. Project [bkTableTitle] / [bkTableFooter] for markup. |
scroll | { x?, y? } | {} | x = min table width (horizontal scroll); y = max body height (pins the header). |
closeOverlaysOnScroll | boolean | true | Close any open select, popover, menu, … opened from a cell when the table scrolls on either axis. Uses the public bkCloseOverlaysOnScroll directive. |
loading | boolean | false | Overlay spinner. |
noResult | BkTableNoResult | {} | Empty state. { text?, image? }, pass just the field you want to override. |
showPagination | boolean | true | Render the pager inside the frame, below the body. |
frontPagination | boolean | true | Off = server paging; supply total instead. |
pageIndex / pageSize | model<number> | 1 / 25 | Two-way. pageSize must be one of the sizes bk-pagination offers (25/50/75/100), or its select renders blank. |
total | number | 0 | Row count in server mode. |
virtualItemSize | number | 0 | Row height in px. Above 0 turns on virtual scroll. |
virtualTrackBy | (i, item) => any | by index | Identity for the virtual viewport. |
stableColumnWidth | boolean | true | Freeze column proportions after first layout, so sorting and filtering cannot resize them. |
Outputs
| Output | Type | Default | Notes |
|---|---|---|---|
queryParams | BkTableQueryParams | – | Everything the server needs to reproduce the view: page, sorts, filters. |
currentPageDataChange | readonly T[] | – | The rows actually rendered, after filter, sort and paging. |
Read from the template ref
| Member | Type | Default | Notes |
|---|---|---|---|
renderData() | readonly T[] | – | The rows to loop in your tbody. Never iterate the raw input. |
totalCount() | number | – | Row count before paging. |
clearSort() / clearFilter() | void | – | Clear every column. |
reset() | void | – | Both, plus back to page 1. |
remeasureColumns() | void | – | Drop the frozen widths and measure again. |
th[bk-th]
Grouped headers use the native colspan / rowspan attributes, no input for them, because none is needed.
Inputs
| Input | Type | Default | Notes |
|---|---|---|---|
columnKey | string | null | Identifies the column in queryParams. Required for server-side work. |
width | string | null | Explicit width. Required in virtual-scroll mode; never overwritten by the width freeze. |
sortFn | (a, b, order) => number | true | null | A comparator sorts client-side; true marks the column sortable and leaves the work to the server. |
sortOrder | model<'ascend' | 'descend' | null> | null | Two-way. |
sortDirections | BkSortOrder[] | ['ascend','descend',null] | The cycle a click steps through. |
sortPriority | number | null | Higher wins in multi-sort. |
filters | BkFilterOption[] | [] | { text, value, byDefault? }. Empty hides the funnel. |
filterFn | (selected[], row) => boolean | true | null | Predicate, or true to defer to the server. |
filterMultiple | boolean | true | Checkboxes when true, radios when false. |
checkbox / checked / indeterminate / disabled | boolean | false | Select-all cell. |
selections | BkTableSelection[] | [] | { text, onSelect }, adds a caret menu beside the checkbox. |
left / right | boolean | string | false | Pin the column. true measures the offset; a string sets it. |
ellipsis / breakWord | boolean | false | Truncate, or allow breaks inside words. |
align | 'left' | 'center' | 'right' | null | Horizontal alignment. |
Inputs
| Input | Type | Default | Notes |
|---|---|---|---|
checkbox / checked / indeterminate / disabled | boolean | false | Row checkbox. checked is two-way. |
showExpand / expand | boolean | false | Expand toggle. expand is two-way; leaves keep the toggle’s width. |
expandIcon | TemplateRef<{ $implicit: boolean }> | null | Custom icon; receives the open state. |
indentLevel / indentSize | number | 0 / 20 | Tree indent, in levels × px. |
treeCell | boolean | false | Marks the column carrying the tree, so leaf labels stay aligned. |
left / right / ellipsis / breakWord / align | – | – | Same as th. |
Directives and helpers
| Selector | Type | Default | Notes |
|---|---|---|---|
[bkTableTitle] / [bkTableFooter] | directive | – | Rich title/footer bars, when a string is not enough. |
tr[bkExpand] | directive | – | Detail row; hidden unless [bkExpand]="true". |
tfoot[bkTableSummary] | directive | – | Summary row. [fixed]="true" pins it to the bottom. |
[bkTableDrag] | directive (exportAs "bkTableDrag") | – | Makes CDK row dragging work in a table: preview widths and edge auto-scroll. Flat lists. |
<bk-drag-handle> | component | – | The grip a row is dragged by. Composes cdkDragHandle via hostDirectives. Own column or inline with another; icon input swaps the default grip for any TemplateRef. |
[bkTreeDrag] | directive (exportAs "bkTreeDrag") | – | Tree-aware row dragging: before / after / inside, with a live drop indicator and hover-to-expand. Extends BkTableDrag. |
[bkVirtualScroll] | directive | – | Row template the viewport instantiates. Receives $implicit and index. |
flattenBkTreeData() | function | – | Flattens a tree to rows, keeping only branches whose ancestors are open. Returns BkTreeRow<T>[]. |
moveBkTreeNode() | function | – | Applies a resolved drop (before / after / inside) to the tree behind it. inside inserts as the first child. Refuses a drop into its own descendant. |
containsBkTreeNode() | function | – | Whether a node sits anywhere in another node’s subtree. |
[bkTreeDrag] exportAs: bkTreeDrag
| Member | Type | Default | Notes |
|---|---|---|---|
treeRows | BkTreeRow<T>[] | [] | The flattened rows currently rendered, in render order, same array the tbody iterates. Basis of the hit-testing. |
scope | 'anywhere' | 'siblings' | 'anywhere' | siblings restricts drags to reordering among the node's current siblings, no re-parenting, no inside zone. Use whenever the tree's levels carry fixed meaning. |
canDrop | (dragged, target, position) => boolean | null | Extra veto beyond scope and the built-in cycle guard, e.g. "a person cannot contain a team". |
expandDelay | number | 700 | Ms the pointer must rest on a target’s middle zone before treeExpand fires. |
treeDrop | output<BkTreeDropEvent<T>> | – | { dragged, target, position }, emitted once per completed drag onto a legal target. |
treeExpand | output<BkTreeRow<T>> | – | Fires after expandDelay of hovering a target’s middle zone. Whether to act is the consumer’s call. |
onDragStart(event, row) / onDragMove(event) / onDragEnd() | method | – | Wire to (cdkDragStarted) / (cdkDragMoved) / (cdkDragEnded) on each row. |
Constraints
These are real limits rather than gaps, ng-zorro has the same ones, for the same reasons.
- Virtual scroll requires uniform row heights, so it does not combine with rowspan, tree data or expandable rows. The viewport derives scroll position from virtualItemSize alone; a variable-height row desynchronises it.
- Virtual scroll requires width on every th. Header and body become two separate tables there, so they can no longer size each other and a shared <colgroup> holds them in alignment.
- Pinned columns must be contiguous from their edge. A gap leaves the columns beyond it overlapping as they scroll underneath.
- Fixed columns need scroll.x. Without a minimum width there is nothing to scroll over and nothing to pin against.
- Frozen columns no longer widen to fit content. That is the point of stableColumnWidth, but it means a long unbreakable value overflows instead, use ellipsis or breakWord on those columns.
- bkTreeDrag requires cdkDropListSortingDisabled. Its indicator assumes a static row order between the pointer's position and the render list; CDK's own sorting animation would move rows out from under that mapping mid-drag.
- Ellipsis goes on the td, not the th. It sets max-width: 0 on whatever carries it, and table-layout: fixed sizes a column from its header cell alone, putting it there collapses the column regardless of any declared width.
- scroll.x / scroll.y accept any CSS length, vh, %,
calc(), not only pixels, so a responsive body height is already possible today. There is no built-in "fill remaining space" mode beyond that. - Uneven row heights are what make a capped scroll.y look broken, not the cap. A fixed-height scroll region always shows a partial row at its boundary, that's ordinary scrolling. It only looks wrong when one row is taller than its neighbours, usually because a narrow pinned column let a value wrap. Keep those columns single-line with ellipsis.