Responsive capabilities
Opt out of capability UI per viewport
The Computed-Columns Pattern
Responsive tables often need to disable specific capability UI on mobile while keeping it active on desktop: a header sort button and click-to-sort behavior might make room for an app-owned sort sheet, a resize handle might not fit a touch layout, and a pin menu might not be worth a tap target on small screens.
The library stays viewport-agnostic. Feed any reactive condition — CDK BreakpointObserver, a matchMedia signal, a device service, a user preference — into the same column-composition APIs used everywhere else, and rebuild columns inside a computed() keyed on that signal.
readonly isMobile = toSignal(
inject(BreakpointObserver)
.observe('(max-width: 767px)')
.pipe(map((result) => result.matches)),
{ initialValue: false }
);
readonly columns = computed(() =>
withNatTableHeaderActions(baseColumns, {
enableSortActions: !this.isMobile(),
enableColumnPinActions: !this.isMobile()
})
);
Sort, pin, and resize UI are all gated by surface enablers that default off. Bind the same breakpoint signal to [enableSorting], [enablePinning], and [enableColumnResizing] so every capability's header UI drops out together on mobile. The enableSortActions / enableColumnPinActions helper options above are a complementary per-column-action layer: the header control shows only when both the surface enabler and the helper option resolve true.
<nat-table-surface
[enableSorting]="!isMobile()"
[enablePinning]="!isMobile()"
[enableColumnResizing]="!isMobile()"
[(state)]="tableState">
<nat-table [columns]="columns()" [data]="data" accessibleName="Responsive capabilities demo table" />
</nat-table-surface>
When the breakpoint signal flips, the computed re-wraps the columns and the headers re-render. Applying withNatTableHeaderActions(...) repeatedly is safe: it unwraps previously wrapped headers before installing the next wrapper, so reactive column builders can compose it without nesting the generated controls.
Capability Opt-Out Table
| Capability | Mobile opt-out |
|---|---|
| Sort button and indicator | enableSortActions: false (helper option) |
| Column resizing (handle, drag, keyboard) | [enableColumnResizing]="false" (surface) |
| Pin menu | enableColumnPinActions: false (helper option) |
| Reorder menu | enableColumnReorderActions: false (helper option) |
Programmatic sorting (setSorting, sort sheet) |
Never disabled |
enableSortActions: false removes the sort button and indicator from the header; there is no click or keyboard sort interaction because the control that would host it is gone. aria-sort on the header cell is unaffected — it announces the column's sort state, not its affordance, so screen readers still hear that a column is sorted when an app-owned control drove the sort. Per-column meta.headerActions.enableSortActions overrides the helper-level option for a single column, following the same resolution order as enableColumnPinActions and enableColumnReorderActions.
TanStack sorting stays fully enabled behind enableSortActions: false: table.setSorting(...) and columnDef-level enableSorting behave exactly as before. This library never maps the mobile opt-out to TanStack's table-level enableSorting option — see the Usage Boundary below for why.
Usage Boundary
Viewport detection belongs to the consuming application, not the table. Wire BreakpointObserver, a matchMedia signal, or a device/user-preference service into the computed-columns pattern above; the library adds no breakpoint config, media-query handling, or new dependency.
TanStack table-level enableSorting is intentionally never used to implement this opt-out. A runtime check against the installed @tanstack/table-core shows that with enableSorting: false, table.setSorting(...) writes the sorting state but getSortedRowModel silently drops it — rows never sort, because the row model filters sorting state by getCanSort(), which returns false when the TanStack table-level flag is off. Mapping the mobile opt-out to that flag would silently break any app-owned sort sheet that sorts programmatically. enableSortActions only ever touches the header action UI.
Opt out of capability UI on mobile
Rebuilding columns in a computed() hides sort, pin, and resize UI on mobile while programmatic sorting stays live.
<nat-table-surface [enableSorting]="!isMobile()" [enablePinning]="!isMobile()" [enableColumnResizing]="!isMobile()" [(state)]="tableState">
<nat-table [columns]="columns()" [data]="data" accessibleName="Responsive capabilities demo table" />
</nat-table-surface>
<button type="button" (click)="sortByValue()">Sort by Value</button>readonly isMobile = toSignal(
inject(BreakpointObserver)
.observe('(max-width: 767px)')
.pipe(map((result) => result.matches)),
{ initialValue: false }
);
readonly columns = computed(() => {
const mobile = this.isMobile();
return withNatTableHeaderActions(baseColumns, {
enableSortActions: !mobile,
enableColumnPinActions: !mobile
});
});
// The sort sheet writes the same state slice the (now hidden) header sort
// button would have written, so sorting keeps working when the UI is hidden.
sortByValue(): void {
this.tableState.update((current) => ({
...current,
sorting: [{ id: 'value', desc: true }]
}));
}