ng-bootstrap HomeBasicAlertFormsFloating labelsInput groupMulti-rangeRangePhone inputSelectRadioCheckboxDatepickerTimepickerDateTime pickerContainersGridCardTab controlRatingBadgeBreadcrumbButton groupButton typeCalendarMarqueeList groupTreeviewTree selectTableClosePaginationPlaceholderSpinnerColor pickerProgress barFor directiveIconOverlaysModalsOffcanvasTooltipToastPopoverDropdownTypeaheadContext menuShellDropdown menu (WC)AdvancedCopyCode snippetPipesTrust-htmlSplit-stringLinifySlugifyWord-countFont-colorHas-propertyToggle buttonsSticky footerAutofocusTrackByAsync HostBindingInstance ofIs interfaceResizableOrdinal numbersMarkdownParallaxPriority navigationScrollspyFile uploadSignature padLazy-loading componentsUser-agentNavigation lockNavigation lock (master/detail)EnterpriseCarouselSchedulerTimelineDockRibbonTile managerDatatablesFile managerQuery builderOTP inputNavbarSplitterAccordionChartsAnimationsSlide up/downFade in/outColor transitionAdditional samplesCollapseFocusTrapDrag-dropTree-select drag-dropQR-code viewerStepperThemingAnchor scrollingSection 1Section 2Section 3Section 4Section 522.25.0

Datatable

Server-side paginated table with sortable headers, optional virtual scrolling, and single / multiple row selection. The component is fetch-driven — provide a BsDatatableFetch callback that resolves a PaginationResponse, and the table handles paging UI, sort state, and (optionally) the virtual viewport.

Styling — this component renders in the light DOM

It has no shadow root. Anything you render into it — row templates, node templates, cell renderers — stays in the document, so your app's stylesheet, your own component styles and Bootstrap's utility classes reach it normally.

Its own CSS is scoped at build time onto a data-mps attribute, the same device Angular uses for _ngcontent, so it cannot leak out onto your markup.

The trade-off runs the other way: page CSS now reaches this component's internals, exactly as emulated encapsulation behaves everywhere else. ::part() and ::slotted() no longer address it — use ordinary CSS selectors instead.

Basic usage

Sortable columns

Column resizing

Drag the line at the right edge of a header, or focus it and use the keyboard. A tap or click on it, without dragging, opens the resize options instead.

  • Tab reaches each column's resize handle.
  • ← / → make the column 10 px narrower or wider.
  • Enter opens the resize options: narrower, wider, fit to content, reset. Esc closes them.

Row selection

Two selection styles. In the default multiple mode a row click selects (Ctrl+click toggles, Shift+click selects a range) and then emits (rowClick), which is what the Basic usage table above does. In checkbox mode, below, a row click only opens the row: selecting happens in the checkbox cell alone, anywhere inside it.

The selection is keyed by [rowKey], so it survives paging, sorting and reload(): tick rows here, move to another page, sort, and the count keeps every key. With [fetch] that key must be stable, taken from the row itself and never from its position. [rowLabel] names each row checkbox for assistive technology ("Select Coldplay").

Selected 0 artist(s)·Opened: —

Selected keys: —

Virtual scrolling — lazy windowed fetch

With [virtualScroll] + [fetch] the table fetches only the pages whose rows are in (or near) the viewport — against the same real, server-paged artist endpoint as the basic section. Scroll and watch the fetch log below: it never drains the whole result set up front; placeholder rows hold the scroll position until each window arrives. Sorting or changing settings drops the cached windows and refetches the visible range.

Pages fetched: — (0 so far)

Tree mode — expandable rows

Click a chevron to expand a row. Children load lazily on first expand and stay cached for subsequent collapse/expand cycles. Placeholder rows reserve viewport space while the fetch is in flight so the scrollbar stays accurate. Cascading selection: checking a parent selects all currently-loaded descendants; partially-selected parents render an indeterminate state.

Column filters

Mark a column filterable and the table grows a second header row with a dropdown trigger in that column. Columns without it get an empty, correctly-sized cell, so the row stays aligned. No filterable column, no second row at all — toggle the checkbox below to watch the row appear and disappear.

A filterable column gets a built-in panel for free: a search box, an include/exclude toggle, a checkbox list of the column's distinct values, and a clear button. It lives in the web component, so React and Vue get the same panel with no wrapper code. The Artist column below uses it as-is.

A checkbox list is the wrong question for a quantity, so a column can ask a different one: filterMode: 'comparison' renders an operator (=, ≠, <, ≤, >, ≥) and a single input, with filterInputType choosing number (the default) or date — the Year started column below. Still the built-in panel: no template and no styling of your own. Narrow the operator list with filterOperators when only some make sense.

Quantities only — there is no text input type and no contains operator. Comparing strings is either exact match, which the value list already does better, or a lexicographic >, which is almost never what anyone means. A free-text filter is a nest-your-own case, below.

The consumer picks the mode per column and the component never infers it: a numeric column is often an enum and a string column is often ordinal, so any guess would be wrong about half the time.

When neither built-in question fits, nest a *bsDatatableFilterPanel inside the column and render whatever you like — the Year quit column does. The template receives the loaded values as a signal and a context object for driving the search and the selection. That markup is yours, so you style it yourself: Bootstrap's .form-control is only styled inside bs-* components in this workspace, and the panel renders in the document-root overlay, outside the table's DOM.

The panel opens in an overlay at the document root, so it is not clipped by the table's scroll container and is not hidden behind the sticky header — this demo runs in virtual-scroll mode, where both would otherwise happen.

The component decides nothing about what a filter means. It collects a selection and emits it; the page below holds an unfiltered master copy, applies the selection itself, and sets filterActive and filterSummary back on the column. That is what lets the same row drive a client-side match here and a server query elsewhere.

Keyboard:Tab reaches each filter trigger; Enter or Space opens the panel and moves focus to its search box; Tab cycles within the open panel; Esc closes it and returns focus to the trigger. Clearing a filter moves focus back to the search box, because the clear button disables itself.

Keyboard, rows — default (multiple) mode:
  • ↑ / ↓ move between rows.
  • Enter or Space selects the row, then opens it (rowClick) — exactly like a click.
  • Ctrl+Space toggles the row and Shift+Space selects a range, the same modifiers as Ctrl+click and Shift+click.
  • → / ← expand / collapse a tree row.
  • Tab reaches each row checkbox; Space on it toggles that row.
Keyboard, rows — checkbox mode:
  • ↑ / ↓ move between rows.
  • Enter opens the row (rowClick) and never selects it.
  • Space toggles the row's checkbox and does not open it. There are no ranges.
  • → / ← expand / collapse a tree row.
  • A click anywhere in the checkbox cell toggles the row; a click elsewhere only opens it.
Show the filter row Showing 138 of 138 artist(s).