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.
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.
Set one fetch callback and the web component owns the whole loop: it loads page 1, derives the total from the response, and fetches each window only as its rows scroll into view. This list is the 1000 seeded orders from apps/api; scroll it and watch the fetch log — it never drains every page. No totalRecords prop, no event bridge.
Pages fetched: — (0 so far)
With selectionMode="checkbox" a row click only opens the row (onRowClick); selecting happens in the checkbox cell alone. The selection is keyed by rowKey — which must be stable with fetch — so it survives paging and sorting: tick a row, change page, and the count keeps it. rowLabel names each checkbox for assistive technology.
Selected 0 order(s) · Opened: —
Keyboard, rows: ↑/↓ move between rows; →/← expand/collapse a tree row. In checkbox mode Enter opens the row and Space toggles its checkbox. In the default multiple mode Enter or Space selects the row and then opens it, with Ctrl toggling and Shift selecting a range — the same modifiers as a click.
The same single fetch callback, branching on req.parentId for roots vs. children. The WC paginates roots lazily and loads children on expand; selected row objects arrive on onSelectionChange as detail.selectedRows.
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. Drop them all and the row is not rendered at all — toggle the checkbox to see it.
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 there is no React code behind it at all — the Artist column below just sets filterable.
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 Founded column below. Still the built-in panel: no renderer, no styling of your own. Narrow the list with filterOperators when only some operators 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.
You pick 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, give the column a filterRenderer — Genre does. It returns a DOM Node (the same contract all three frameworks see) and must return a stable one: the element mounts it once per open, so repainting it is the renderer's job. That markup is yours, so you style it — Bootstrap's .form-control is only styled inside bs-* components here, and the panel renders in the document-root overlay, outside the table.
The panel opens in an overlay at the document root, so it is not clipped by the scroll container and 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 on onFilterChange; this page holds the unfiltered master copy, applies the selection, and sets filterActive and filterSummary back on the column.
Keyboard: Tab reaches each trigger, Enter/Space opens the panel and moves focus to its search box, Tab cycles inside it, Esc closes and returns focus to the trigger. Clearing a filter moves focus back to the search box, because the clear button disables itself.