BrickclayDesign System

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.

bk-tableData displayAngular

Preview

A working preview of Table in the Brickclay style. Try it.

Order NoCustomerAmountStatusCreated
#1042Northwind$12,400Paid2026-10-02
#1043Contoso$3,180Pending2026-10-05
#1044Fabrikam$8,900Paid2026-10-11
#1045Tailspin$1,250Overdue2026-10-14
#1046Wingtip$22,070Paid2026-10-20
Total$47,800
Interact with the preview to see events here.

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.

NameAgeDepartmentAddress
John Brown24Engineering10 Lake Park, London No. 1
Jim Green31Design11 Lake Park, London No. 2
Joe Black38Finance12 Lake Park, London No. 3
Jane Cooper45Operations13 Lake Park, London No. 4
Cody Fisher52Engineering14 Lake Park, London No. 5
Esther Howard59Design15 Lake Park, London No. 6
Kristin Watson30Finance16 Lake Park, London No. 7
Cameron Williamson37Operations17 Lake Park, London No. 8
Brooklyn Simmons44Engineering18 Lake Park, London No. 9
Leslie Alexander51Design19 Lake Park, London No. 10
John Brown58Finance20 Lake Park, London No. 11
Jim Green29Operations21 Lake Park, London No. 12
Joe Black36Engineering22 Lake Park, London No. 13
Jane Cooper43Design23 Lake Park, London No. 14
Cody Fisher50Finance24 Lake Park, London No. 15
Esther Howard57Operations25 Lake Park, London No. 16
Kristin Watson28Engineering26 Lake Park, London No. 17
Cameron Williamson35Design27 Lake Park, London No. 18
Brooklyn Simmons42Finance28 Lake Park, London No. 19
Leslie Alexander49Operations29 Lake Park, London No. 20
John Brown56Engineering30 Lake Park, London No. 21
Jim Green27Design31 Lake Park, London No. 22
Joe Black34Finance32 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.

NameAgeDepartmentAddressAction
John Brown24Engineering10 Lake Park, London No. 1EditDelete
Jim Green31Design11 Lake Park, London No. 2EditDelete
Joe Black38Finance12 Lake Park, London No. 3EditDelete
Jane Cooper45Operations13 Lake Park, London No. 4EditDelete
Cody Fisher52Engineering14 Lake Park, London No. 5EditDelete
Esther Howard59Design15 Lake Park, London No. 6EditDelete

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.

NameAgeDepartmentStatus
John Brown24EngineeringInactive
Jim Green31DesignActive
Joe Black38FinanceActive
Jane Cooper45OperationsInactive
Cody Fisher52EngineeringActive
Esther Howard59DesignActive
Kristin Watson30FinanceInactive
Cameron Williamson37OperationsActive

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.

NameAgeDepartmentStatus
John Brown24EngineeringInactive
Jim Green31DesignActive
Joe Black38FinanceActive
Jane Cooper45OperationsInactive
Cody Fisher52EngineeringActive
Esther Howard59DesignActive

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.

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations

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.

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations
Cody Fisher52Engineering
Esther Howard59Design
Kristin Watson30Finance
Cameron Williamson37Operations
Brooklyn Simmons44Engineering
Leslie Alexander51Design

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"

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations

size = "default"

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations

size = "md"

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations

size = "sm"

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations

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.

NameAgeDepartment
John Brown24Engineering
John Brown, 10 Lake Park, London No. 1. Status Inactive. Even-numbered rows use the custom + / − expand icon.
+Jim Green31Design
Jim Green, 11 Lake Park, London No. 2. Status Active. Even-numbered rows use the custom + / − expand icon.
Joe Black38Finance
Joe Black, 12 Lake Park, London No. 3. Status Active. Even-numbered rows use the custom + / − expand icon.
+Jane Cooper45Operations
Jane Cooper, 13 Lake Park, London No. 4. Status Inactive. Even-numbered rows use the custom + / − expand icon.
Cody Fisher52Engineering
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.

NameAgeDepartmentAddress
John BrownShared cohort (rows 1–3)Engineering10 Lake Park, London No. 1
Jim GreenDesign11 Lake Park, London No. 2
Joe BlackFinance12 Lake Park, London No. 3
Jane Cooper45Department and address merged for this row
Cody Fisher52Engineering14 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.

NameRoleHeadcount
ProductDivision42
DesignTeam12
EngineeringTeam30
FinanceDivision8

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.

NameRoleHeadcount
ProductDivision42
DesignTeam12
Esther HowardLead Designer1
Cody FisherDesigner1
EngineeringTeam30
Jim GreenStaff Engineer1
Joe BlackEngineer1
FinanceDivision8
Kristin WatsonController1

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.

NameAgeDepartmentAddressStatusDepartmentAddressAction
John Brown24Engineering10 Lake Park, London No. 1InactiveEngineering10 Lake Park, London No. 1Edit
Jim Green31Design11 Lake Park, London No. 2ActiveDesign11 Lake Park, London No. 2Edit
Joe Black38Finance12 Lake Park, London No. 3ActiveFinance12 Lake Park, London No. 3Edit
Jane Cooper45Operations13 Lake Park, London No. 4InactiveOperations13 Lake Park, London No. 4Edit
Cody Fisher52Engineering14 Lake Park, London No. 5ActiveEngineering14 Lake Park, London No. 5Edit
Esther Howard59Design15 Lake Park, London No. 6ActiveDesign15 Lake Park, London No. 6Edit
Kristin Watson30Finance16 Lake Park, London No. 7InactiveFinance16 Lake Park, London No. 7Edit
Cameron Williamson37Operations17 Lake Park, London No. 8ActiveOperations17 Lake Park, London No. 8Edit
Brooklyn Simmons44Engineering18 Lake Park, London No. 9ActiveEngineering18 Lake Park, London No. 9Edit
Leslie Alexander51Design19 Lake Park, London No. 10InactiveDesign19 Lake Park, London No. 10Edit
John Brown58Finance20 Lake Park, London No. 11ActiveFinance20 Lake Park, London No. 11Edit
Jim Green29Operations21 Lake Park, London No. 12ActiveOperations21 Lake Park, London No. 12Edit
Joe Black36Engineering22 Lake Park, London No. 13InactiveEngineering22 Lake Park, London No. 13Edit
Jane Cooper43Design23 Lake Park, London No. 14ActiveDesign23 Lake Park, London No. 14Edit
Cody Fisher50Finance24 Lake Park, London No. 15ActiveFinance24 Lake Park, London No. 15Edit
Esther Howard57Operations25 Lake Park, London No. 16InactiveOperations25 Lake Park, London No. 16Edit
Kristin Watson28Engineering26 Lake Park, London No. 17ActiveEngineering26 Lake Park, London No. 17Edit
Cameron Williamson35Design27 Lake Park, London No. 18ActiveDesign27 Lake Park, London No. 18Edit
Brooklyn Simmons42Finance28 Lake Park, London No. 19InactiveFinance28 Lake Park, London No. 19Edit
Leslie Alexander49Operations29 Lake Park, London No. 20ActiveOperations29 Lake Park, London No. 20Edit
John Brown56Engineering30 Lake Park, London No. 21ActiveEngineering30 Lake Park, London No. 21Edit
Jim Green27Design31 Lake Park, London No. 22InactiveDesign31 Lake Park, London No. 22Edit
Joe Black34Finance32 Lake Park, London No. 23ActiveFinance32 Lake Park, London No. 23Edit
Jane Cooper41Operations33 Lake Park, London No. 24ActiveOperations33 Lake Park, London No. 24Edit
Cody Fisher48Engineering34 Lake Park, London No. 25InactiveEngineering34 Lake Park, London No. 25Edit
Esther Howard55Design35 Lake Park, London No. 26ActiveDesign35 Lake Park, London No. 26Edit
Kristin Watson26Finance36 Lake Park, London No. 27ActiveFinance36 Lake Park, London No. 27Edit
Cameron Williamson33Operations37 Lake Park, London No. 28InactiveOperations37 Lake Park, London No. 28Edit
Brooklyn Simmons40Engineering38 Lake Park, London No. 29ActiveEngineering38 Lake Park, London No. 29Edit
Leslie Alexander47Design39 Lake Park, London No. 30ActiveDesign39 Lake Park, London No. 30Edit

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.

NamePersonalWork
AgeAddressDepartment | Status
John Brown2410 Lake Park, London No. 1 | Engineering | Inactive
Jim Green3111 Lake Park, London No. 2 | Design | Active
Joe Black3812 Lake Park, London No. 3 | Finance | Active
Jane Cooper4513 Lake Park, London No. 4 | Operations | Inactive
Cody Fisher5214 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.

NameAddressAge
John Brown10 Lake Park, London No. 124
Jim Green11 Lake Park, London No. 231
Joe Black12 Lake Park, London No. 338
Jane Cooper13 Lake Park, London No. 445
Cody Fisher14 Lake Park, London No. 552

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.

NameAgeAddressAction
John Brown2410 Lake Park, London No. 1Edit
Jim Green3111 Lake Park, London No. 2Edit
Joe Black3812 Lake Park, London No. 3Edit
Jane Cooper4513 Lake Park, London No. 4Edit
Cody Fisher5214 Lake Park, London No. 5Edit

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.

NameDepartmentStatus
John BrownEngineeringInactive
InvoiceDateAmountStatusINV-112026-01-101,201PaidINV-122026-02-112,401PendingINV-132026-03-123,601Pending
Jim GreenDesignActive
InvoiceDateAmountStatusINV-212026-01-101,202PaidINV-222026-02-112,402PendingINV-232026-03-123,602Pending
Joe BlackFinanceActive
InvoiceDateAmountStatusINV-312026-01-101,203PaidINV-322026-02-112,403PendingINV-332026-03-123,603Pending
Jane CooperOperationsInactive
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.

#NameAgeAddress
1John Brown2410 Lake Park, London No. 1
2Jim Green3111 Lake Park, London No. 2
3Joe Black3812 Lake Park, London No. 3
4Jane Cooper4513 Lake Park, London No. 4
5Cody Fisher5214 Lake Park, London No. 5
6Esther Howard5915 Lake Park, London No. 6
7Kristin Watson3016 Lake Park, London No. 7
8Cameron Williamson3717 Lake Park, London No. 8
9Brooklyn Simmons4418 Lake Park, London No. 9
10Leslie Alexander5119 Lake Park, London No. 10
11John Brown5820 Lake Park, London No. 11
12Jim Green2921 Lake Park, London No. 12
13Joe Black3622 Lake Park, London No. 13
14Jane Cooper4323 Lake Park, London No. 14
15Cody Fisher5024 Lake Park, London No. 15
16Esther Howard5725 Lake Park, London No. 16
17Kristin Watson2826 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.

NameAgeDepartment
John Brown24Engineering
Jim Green31Design
Joe Black38Finance
Jane Cooper45Operations
Cody Fisher52Engineering
Esther Howard59Design
Kristin Watson30Finance

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-DDDDDelivered 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-XYZReconciliation 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.

NameDepartmentAge
John BrownEngineering24
Jim GreenDesign31
Joe BlackFinance38
Jane CooperOperations45
Cody FisherEngineering52
Esther HowardDesign59
Kristin WatsonFinance30
Cameron WilliamsonOperations37
Total (8 people)316
Average age40

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.

NameEmployeeCoachNotesAddress
John BrownEmployeeCoachView10 Lake Park, London No. 1
Jim GreenEmployeeCoachView11 Lake Park, London No. 2
Joe BlackEmployeeCoachView12 Lake Park, London No. 3
Jane CooperEmployeeCoachView13 Lake Park, London No. 4
Cody FisherEmployeeCoachView14 Lake Park, London No. 5
Esther HowardEmployeeCoachView15 Lake Park, London No. 6
Kristin WatsonEmployeeCoachView16 Lake Park, London No. 7
Cameron WilliamsonEmployeeCoachView17 Lake Park, London No. 8
Brooklyn SimmonsEmployeeCoachView18 Lake Park, London No. 9
Leslie AlexanderEmployeeCoachView19 Lake Park, London No. 10
John BrownEmployeeCoachView20 Lake Park, London No. 11
Jim GreenEmployeeCoachView21 Lake Park, London No. 12
Joe BlackEmployeeCoachView22 Lake Park, London No. 13
Jane CooperEmployeeCoachView23 Lake Park, London No. 14
Cody FisherEmployeeCoachView24 Lake Park, London No. 15
Esther HowardEmployeeCoachView25 Lake Park, London No. 16
Kristin WatsonEmployeeCoachView26 Lake Park, London No. 17
Cameron WilliamsonEmployeeCoachView27 Lake Park, London No. 18
Brooklyn SimmonsEmployeeCoachView28 Lake Park, London No. 19
Leslie AlexanderEmployeeCoachView29 Lake Park, London No. 20
John BrownEmployeeCoachView30 Lake Park, London No. 21
Jim GreenEmployeeCoachView31 Lake Park, London No. 22
Joe BlackEmployeeCoachView32 Lake Park, London No. 23
Jane CooperEmployeeCoachView33 Lake Park, London No. 24
Cody FisherEmployeeCoachView34 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

InputTypeDefaultNotes
datareadonly 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.
borderedbooleanfalseVertical rules between columns.
title / footerstring''Plain-text bars. Project [bkTableTitle] / [bkTableFooter] for markup.
scroll{ x?, y? }{}x = min table width (horizontal scroll); y = max body height (pins the header).
closeOverlaysOnScrollbooleantrueClose any open select, popover, menu, … opened from a cell when the table scrolls on either axis. Uses the public bkCloseOverlaysOnScroll directive.
loadingbooleanfalseOverlay spinner.
noResultBkTableNoResult{}Empty state. { text?, image? }, pass just the field you want to override.
showPaginationbooleantrueRender the pager inside the frame, below the body.
frontPaginationbooleantrueOff = server paging; supply total instead.
pageIndex / pageSizemodel<number>1 / 25Two-way. pageSize must be one of the sizes bk-pagination offers (25/50/75/100), or its select renders blank.
totalnumber0Row count in server mode.
virtualItemSizenumber0Row height in px. Above 0 turns on virtual scroll.
virtualTrackBy(i, item) => anyby indexIdentity for the virtual viewport.
stableColumnWidthbooleantrueFreeze column proportions after first layout, so sorting and filtering cannot resize them.

Outputs

OutputTypeDefaultNotes
queryParamsBkTableQueryParams–Everything the server needs to reproduce the view: page, sorts, filters.
currentPageDataChangereadonly T[]–The rows actually rendered, after filter, sort and paging.

Read from the template ref

MemberTypeDefaultNotes
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

InputTypeDefaultNotes
columnKeystringnullIdentifies the column in queryParams. Required for server-side work.
widthstringnullExplicit width. Required in virtual-scroll mode; never overwritten by the width freeze.
sortFn(a, b, order) => numbertrue | nullA comparator sorts client-side; true marks the column sortable and leaves the work to the server.
sortOrdermodel<'ascend''descend'null> | null | Two-way.
sortDirectionsBkSortOrder[]['ascend','descend',null]The cycle a click steps through.
sortPrioritynumbernullHigher wins in multi-sort.
filtersBkFilterOption[][]{ text, value, byDefault? }. Empty hides the funnel.
filterFn(selected[], row) => booleantrue | nullPredicate, or true to defer to the server.
filterMultiplebooleantrueCheckboxes when true, radios when false.
checkbox / checked / indeterminate / disabledbooleanfalseSelect-all cell.
selectionsBkTableSelection[][]{ text, onSelect }, adds a caret menu beside the checkbox.
left / rightboolean | stringfalsePin the column. true measures the offset; a string sets it.
ellipsis / breakWordbooleanfalseTruncate, or allow breaks inside words.
align'left' | 'center' | 'right'nullHorizontal alignment.

Inputs

InputTypeDefaultNotes
checkbox / checked / indeterminate / disabledbooleanfalseRow checkbox. checked is two-way.
showExpand / expandbooleanfalseExpand toggle. expand is two-way; leaves keep the toggle’s width.
expandIconTemplateRef<{ $implicit: boolean }>nullCustom icon; receives the open state.
indentLevel / indentSizenumber0 / 20Tree indent, in levels × px.
treeCellbooleanfalseMarks the column carrying the tree, so leaf labels stay aligned.
left / right / ellipsis / breakWord / align––Same as th.

Directives and helpers

SelectorTypeDefaultNotes
[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

MemberTypeDefaultNotes
treeRowsBkTreeRow<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) => booleannullExtra veto beyond scope and the built-in cycle guard, e.g. "a person cannot contain a team".
expandDelaynumber700Ms the pointer must rest on a target’s middle zone before treeExpand fires.
treeDropoutput<BkTreeDropEvent<T>>–{ dragged, target, position }, emitted once per completed drag onto a legal target.
treeExpandoutput<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.