LWC Custom Datatable Types
Use this skill when lightning-datatable is the right shell for your grid but the built-in cell types (text, number, url, email, button, boolean, date, currency, percent, phone) cannot render or edit the column the way the feature demands. This skill covers subclassing the base datatable, registering customTypes, writing display and edit templates, integrating with sorting and inline edit, and avoiding the traps that silently break the extension.
Before Starting
Gather this context before cutting code:
- Is the cell purely presentational (pill, bar, icon), or must it be inline-editable through the datatable's draft-values flow?
- What primitive value represents the cell for sorting purposes? Sorting compares the column's
value, not the rendered DOM. - How many rows will render at once?
lightning-datatablevirtualizes rows internally, but an expensive custom template multiplies that cost every scroll. - Is the column safe to render (no raw HTML) or does it need sanitization before display?
- Will this extension be packaged?
customTypesare defined per-subclass, so two downstream skills that each need a different custom type cannot be combined without building a merged subclass.
Core Concepts
Custom cell types in lightning-datatable are a subclassing mechanism, not a slot API. The subclass registers types by name and points each one at a template .html file that sits next to the component. The base datatable then uses those templates when a column's type matches a registered custom type.
Subclass LightningDatatable And Declare customTypes
The extension pattern is fixed by the platform: import the base class, extend it, and declare a static customTypes object whose keys are the new type names.
import LightningDatatable from 'lightning/datatable';
import customPill from './customPill.html';
import customPillEdit from './customPillEdit.html';
export default class CustomDatatable extends LightningDatatable {
static customTypes = {
statusPill: {
template: customPill,
editTemplate: customPillEdit, // optional — only if inline-editable
standardCellLayout: true, // reuse built-in padding, focus, edit icon
typeAttributes: ['variant', 'label'] // MUST list every key you want to read
}
};
}
The typeAttributes array is the most important and the most missed piece. It names the keys the template is allowed to read out of the column's typeAttributes binding. If a key is not in that array, the datatable silently strips it, the template sees undefined, and nothing visibly fails — the cell just renders blank or default.
Templates Are Data-Bound, Not Component Instances
The template and editTemplate HTML files are ordinary LWC template fragments, but they are evaluated against a fixed binding surface — {value, typeAttributes, editable, context, keyField} and the edit-template's editing equivalents. They have no this, no lifecycle hooks, no imperative handlers. A template cannot call connectedCallback, dispatch events from JavaScript, or hold state. If the cell needs interactive state (a multi-step toggle, debounced search, rich validation), the template must render a child LWC and delegate.
Edit Templates Must Wire To value For Draft-Values
lightning-datatable emits draft-values when an inline edit is saved, and it discovers the new cell value by reading the input element's value property inside the edit template. An editTemplate that uses, for example, lightning-combobox must bind value={typeAttributes.options} for choices and set value={value} for the current cell value so the datatable's harness can capture the draft. Miss this wiring and the cell appears editable, but the Save button never lights up.
Sorting Uses value, Not Rendered Output
The base datatable sorts by the column's primitive value, not by what the template paints. A status pill rendered from {color: 'red', label: 'Hot'} needs either a flat scalar in value or an explicit sortBy on the column so sort order matches the displayed order. Filtering through lightning-datatable itself does not ship — filtering is the consumer's job — but the same principle applies: whatever you filter against should be a stable scalar on the row.
Custom Templates Compete For The Virtualization Budget
lightning-datatable virtualizes rows so that only visible rows are in the DOM. That budget is small. A lightweight template (a span with an SLDS class) is cheap; a lightning-record-edit-form or a nested datatable inside a cell multiplies per-row cost and breaks smooth scrolling at a few hundred rows. For any cell beyond a few SLDS-decorated elements, extract it into a child LWC and keep the template itself trivial.
Common Patterns
Status Pill / Badge Column
When to use: A column displays a categorical state (open/closed, healthy/warning/error) and needs color coding.
How it works: Register a statusPill custom type whose template renders <span class={pillClass}>{typeAttributes.label}</span>, driving pillClass from typeAttributes.variant. Keep the primitive value on the column as the sortable string so the built-in sort behaves sensibly.
Why not the alternative: Using type: 'text' and dumping a status word into the cell loses the visual affordance; wrapping every row in a child LWC that ingests the full record is heavier than the SLDS badge markup needs.
Progress Bar Column
When to use: A numeric cell benefits from a bar (percent complete, score, SLA remaining).
How it works: Register a progress custom type whose template renders <lightning-progress-bar value={value} variant={typeAttributes.variant}>. Keep value as the numeric percent so sorting works without extra config.
Why not the alternative: Writing the percent as text hides scale; computing the bar width by hand in the template without lightning-progress-bar skips the accessibility attributes that SLDS ships.
Editable Picklist Column
When to use: A column needs a dropdown edit without opening a row-level modal.
How it works: Register editPicklist with both template (read-only text span) and editTemplate (lightning-combobox whose options come from typeAttributes.options and whose value is bound to value). The datatable harness picks up the draft on blur or change and emits draft-values; the consumer saves them.
Why not the alternative: Navigating to a separate edit screen for a single-field change is slow and breaks the datatable's bulk-edit story.
Action Cell With Multiple Buttons
When to use: Each row needs more than one action and the built-in type: 'action' menu is too limiting.
How it works: Register an actionRow type with a template that renders a horizontal stack of lightning-button elements. Dispatch a row event via a small child LWC if state (e.g., loading spinner) matters.
Why not the alternative: Embedding a full lightning-button-group with dynamic children inline in the template is fine; embedding a full form is not — that pattern belongs in a row-detail sub-LWC.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Categorical status badge or pill | Custom type with SLDS badge classes, driven by typeAttributes.variant |
Cheap template, preserves virtualization budget |
| Numeric progress indicator | Custom type wrapping lightning-progress-bar |
Accessible and consistent with platform UI |
| Per-row action buttons | customTypes with an actions array surfaced through typeAttributes |
Keeps action logic row-scoped and event-driven |
| Full row-level edit form | Extract to a sub-LWC detail view, not a custom cell | A cell that hosts a form kills the virtualization budget |
| Editable picklist with known options | editTemplate using lightning-combobox wired to value |
Integrates with built-in draft-values save flow |
| Column needs to sort on a computed key | Keep value as the comparable scalar; put display data in typeAttributes |
Datatable sorts on value, not rendered DOM |
Recommended Workflow
- Confirm the column cannot be expressed as a built-in datatable type (re-read the base
lightning-datatabletype list). - Design the row shape: what scalar goes on
value, what goes ontypeAttributes, and whether the column is editable. - Scaffold the subclass:
extends LightningDatatable, declarestatic customTypes, import each template, and list every key intypeAttributes. - Build the display template first, verify rendering on a small dataset, then add the
editTemplateand wirevaluesodraft-valuesflows correctly. - Profile with 200 and 1,000 rows; if scrolling lags, move interactive content into a child LWC and keep the template trivial.
- Add sort and filter behavior by keeping
valuecomparable and documenting anysortByoverride. - Run
scripts/check_lwc_custom_datatable_types.pyagainst the component source to catch missingtypeAttributesarrays and orphaned template filenames.
Review Checklist
- The component
extends LightningDatatableand declaresstatic customTypes. - Every registered custom type has a
typeAttributesarray listing every key its template reads. - Each
templateandeditTemplatepoints to a real sibling.htmlfile next to the.js. - The
editTemplatebinds the editable control'svaluetovalueso draft-values fire. - The column's
valueis a stable scalar suitable for sorting and filtering. - No template contains a
<lightning-record-edit-form>, nested datatable, or heavy child tree. - Scrolling on a representative dataset does not drop frames in a production-like org.
Salesforce-Specific Gotchas
typeAttributesnames not listed are silently dropped — a typo in the array or forgetting the array entirely makes the bindingundefinedwith no console warning.- Templates have no
thisand no lifecycle — imperative handlers,renderedCallback, andLightningElementlifecycle APIs do not exist in the binding surface. - Sorting ignores rendered DOM — the column's primitive
valuedrives sort order, so two rows with different pills but the samevalueappear equal. draft-valuesrequires thevaluebinding on the edit control — usingselected-valueon a combobox or writing to a local proxy breaks the save path.- Custom types are per-subclass — two separate extensions cannot be mixed in one datatable instance; you must build a merged subclass that registers both.
- Overriding base datatable CSS is brittle — prefer SLDS Styling Hooks where available; direct
.slds-tableoverrides can break on minor-release DOM changes. - Managed-package compatibility — a subclass declared in a managed namespace renders fine, but consumers cannot add new custom types on top without a new subclass in their own namespace.
Output Artifacts
| Artifact | Description |
|---|---|
| Custom datatable subclass (.js + .js-meta.xml) | The extends LightningDatatable component with static customTypes and template imports |
Display / edit template .html files |
One per custom type for render; one more per editable type for inline edit |
| Column configuration guidance | Sample column array showing type, typeAttributes, and editable wiring for the host component |
| Checker output | Findings from check_lwc_custom_datatable_types.py: missing typeAttributes, orphaned templates, non-subclass usage |
Related Skills
lwc/lwc-data-table— use when the column set fits the built-in types and you do not need a subclass.lwc/virtualized-lists— use when the dataset is large enough that even a well-behaved datatable is the wrong tool.lwc/lwc-forms-and-validation— use when the edit story is form-shaped rather than cell-shaped (per-row modal or record-edit-form).