A sortable table looks simple until users depend on it to find the one overdue invoice, highest-value lead, or inventory item about to run out. An Angular data grid with sorting needs more than clickable headers. It needs predictable sort rules, clear visual state, stable results, and a design that still performs when the dataset grows.
For a small static list, a hand-built `Array.sort()` call may be enough. For an operations dashboard, CRM, or reporting screen, sorting quickly becomes part of the application’s data model and user workflow. Getting those decisions right early prevents the familiar cycle of patching headers, special-casing dates, and rebuilding the table when server-side data arrives.
What sorting should do in an Angular data grid
Sorting answers a specific question: in what order should users inspect these rows? The answer changes by column. Names usually need locale-aware alphabetical comparison. Revenue needs numeric comparison. Dates need chronological comparison, not string comparison. A status column may need a product-defined order such as `Critical`, `At Risk`, `On Track`, rather than alphabetical order.
That is why a grid should treat sorting as column behavior, not a generic afterthought. Each sortable column needs a field or accessor, a comparator where the default is not appropriate, and a visible direction. The grid also needs to define what happens to blank values. Putting blanks first may help data-cleanup workflows; putting them last often makes business reporting easier to scan. Either choice is valid. Inconsistency is the problem.
A useful sorting interaction has three states: ascending, descending, and unsorted. The unsorted state matters when users need to return to the original API order or a default business order. A sort indicator should be visible without becoming visual noise, and the active column should be distinguishable from inactive headers.
Single-column versus multi-column sorting
Single-column sorting is the right default for many product tables. It is easy to understand: click `Amount`, see the highest or lowest values first. Multi-column sorting earns its place when ties are meaningful. A sales team might sort by region, then by account name. A manufacturing team might sort by priority, then due date.
Multi-sort needs an explicit interaction model, usually Shift-click or a clearly labeled control. Do not add it solely because enterprise grids support it. Every extra behavior has a documentation, testing, and accessibility cost. If users rarely need secondary ordering, a stable single-column sort may already give them the outcome they expect.
Keep the sort state separate from the rows
A maintainable Angular implementation does not permanently mutate the source array just because a user clicked a header. Keep the raw rows, current sort field, and sort direction as separate state. Then derive the displayed rows from that state.
Here is a compact example using Angular signals:
```ts import { computed, signal } from '@angular/core';
type SortDirection = 'asc' | 'desc' | null; type SortField = 'customer' | 'amount' | 'createdAt';
interface Invoice { customer: string; amount: number; createdAt: string; }
const rows = signal
const sortedRows = computed(() => { const field = sortField(); const direction = sortDirection(); const source = rows();
if (!field || !direction) return source;
return [...source].sort((a, b) => { const result = compareValues(a[field], b[field]); return direction === 'asc' ? result : -result; }); });
function compareValues(a: string | number, b: string | number): number { if (typeof a === 'number' && typeof b === 'number') return a - b; return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }); } ```
The spread operator is deliberate. JavaScript’s `sort()` mutates its array. Sorting a copied array protects the source order, makes reset behavior straightforward, and avoids surprising other parts of the component that may reference the same data.
Your header event can cycle through the three states. If a user clicks a new field, start ascending. If they click the active field, move from ascending to descending, then clear the sort. This is a small interaction detail that makes a grid feel complete rather than improvised.
Use comparators that match real business data
The generic comparator above is a starting point, not a universal answer. Dates are a common failure point. ISO timestamps such as `2026-07-22T14:30:00Z` can be sorted as strings, but localized display values such as `7/22/2026` should never be. Sort the underlying timestamp or epoch value and format only for display.
Currency should sort on a number, not on a formatted value containing dollar signs and commas. Percentages should sort on their numeric representation. For a status field, use a rank map:
```ts const statusRank: Record
function compareStatus(a: string, b: string): number { return (statusRank[a] ?? 99) - (statusRank[b] ?? 99); } ```
Locale-aware text comparison is also worth using for customer names, products, and mixed identifiers. `localeCompare()` with `numeric: true` places `Item 9` before `Item 10`, which is usually what people mean by alphabetical sorting in a business interface.
Null handling deserves the same attention. Define it once in a shared comparator instead of letting every column behave differently. For example, return blank values last regardless of direction, or reverse them with the rest of the values. The better option depends on the task. A finance team reviewing missing data may want blanks first. A user scanning highest revenue accounts probably does not.
Client-side sorting has a ceiling
Client-side sorting is fast and convenient when the browser already holds the relevant rows. It works well for a few hundred or several thousand records, depending on row complexity, device performance, filtering, rendering strategy, and whether virtual scrolling is in play.
It becomes misleading when the grid displays one page of a much larger dataset. Sorting only the current 50 rows is not the same as sorting all 500,000 invoices. Users will assume the result is global unless the interface says otherwise.
For paginated APIs and large datasets, send sort state to the server as query parameters or request payload fields, such as `sort=amount` and `direction=desc`. The server should apply the order before pagination, then return the rows and total count. When the user changes sorting, reset the page to one and fetch again.
Server-side sorting adds request latency and requires a backend contract, but it gives correct global ordering and keeps memory pressure off the browser. It also requires defensive design: allow only known sort fields, map UI fields to database columns safely, and ensure the database has indexes that support the most common sort paths.
Make sorting accessible and testable
A clickable header made from a plain `div` creates extra work. Use a real button inside the header, or use a grid component that already provides keyboard behavior and appropriate ARIA state. Keyboard users should be able to focus the control, activate it with Enter or Space, and understand the active direction through visible text or `aria-sort` semantics.
Test sorting at two levels. Unit-test each comparator with values that expose edge cases: nulls, lowercase names, `Item 2` versus `Item 12`, invalid dates, and tied values. Then test the component workflow: clicking a header updates the indicator, changes row order, and resets pagination when the sort is server-driven.
For data-heavy applications, a production-ready grid saves more than code. A purpose-built Angular package such as Simple Table can provide sorting alongside filtering, pagination, virtual scrolling, column resizing, pinning, and TypeScript definitions without assembling separate table plugins. That trade-off is especially useful when the table is a product surface, not a one-off admin view.
Choose the smallest approach that stays correct
A custom sortable table is reasonable when the dataset is small, the columns are fixed, and the interaction is genuinely simple. Once requirements include remote data, column-specific sorting, editable cells, filtering, export, or virtualized rows, the table has become a grid. Treating it that way early can save weeks of integration work later.
The best sorting experience does not call attention to itself. It gives users the order they expect, preserves that order across paging and refreshes, and leaves your team with code that is still easy to change when the next dashboard requirement arrives.
