Table
Display multiple records in declared columns.
A table block displays multiple records through a declared set of fields. Use it when people must scan and compare records.
The fields control the column order, headings, and display variants. The data supplies the raw rows. The SDK remaps each cell to the protocol format before it sends the node.
API
table(data: Row[]): TableBuilder<Row>
| Method | Input | Result |
|---|---|---|
setFields |
Nodeable<Field>[] |
Sets the columns and their order. |
withFooter |
{ label, values }[] |
Sets summary rows below the data. |
onDefect |
(defect: Defect) => void |
Handles values that the SDK cannot put on the wire. |
Calling setFields or withFooter again replaces the previous
value.
Basic table
Pass the raw rows to table(data), then set the fields. Each declared field becomes one column.
import { defineResource, fields, table } from '@backlit/sdk'
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.text('customer', { label: 'Customer' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view('list', () =>
table([
{
reference: 'ORD-1042',
customer: 'A. Customer',
total: 149.5,
},
{
reference: 'ORD-1043',
customer: 'B. Customer',
total: 89,
},
]).setFields(
orders.pick('reference', 'customer', 'total')
)
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "text",
"name": "customer",
"label": "Customer"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"reference": "ORD-1042",
"customer": "A. Customer",
"total": 149.5
},
{
"reference": "ORD-1043",
"customer": "B. Customer",
"total": 89
}
]
}
]
}
}
}Table inside a panel
Put a table inside a panel when the records need a title, description, filters, or a bounded surface. The table remains the panel content.
import {
defineResource,
fields,
panel,
table,
} from '@backlit/sdk'
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.text('customer', { label: 'Customer' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view('list', () =>
panel([
table([
{
reference: 'ORD-1042',
customer: 'A. Customer',
total: 149.5,
},
{
reference: 'ORD-1043',
customer: 'B. Customer',
total: 89,
},
]).setFields(
orders.pick('reference', 'customer', 'total')
),
])
.setTitle('Recent orders', { icon: 'orders' })
.setDescription(
'The latest orders from all sales channels.'
)
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "panel",
"title": "Recent orders",
"titleIcon": "orders",
"description": "The latest orders from all sales channels.",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "text",
"name": "customer",
"label": "Customer"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"reference": "ORD-1042",
"customer": "A. Customer",
"total": 149.5
},
{
"reference": "ORD-1043",
"customer": "B. Customer",
"total": 89
}
]
}
]
}
}
]
}
}
}Empty table
Pass an empty array when the query returns no records. The protocol contains an empty data array. The renderer supplies the empty state.
import { defineResource, fields, table } from '@backlit/sdk'
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.text('customer', { label: 'Customer' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view(
'list',
() =>
table([]).setFields(
orders.pick('reference', 'customer', 'total')
)
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "text",
"name": "customer",
"label": "Customer"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": []
}
]
}
}
}Selected fields
Use resource.pick to select columns and set their order. Extra keys in a raw
row do not go into the protocol output.
import { defineResource, fields, table } from '@backlit/sdk'
const orders = defineResource('orders', [
fields.number('id', { label: 'ID' }),
fields.text('reference', { label: 'Order' }),
fields.text('customer', { label: 'Customer' }),
fields.text('status', { label: 'Status' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view('list', () =>
table([
{
id: 42,
reference: 'ORD-1042',
customer: 'A. Customer',
status: 'Shipped',
total: 149.5,
},
])
.setFields(orders.pick('reference', 'status', 'total'))
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "text",
"name": "status",
"label": "Status"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"reference": "ORD-1042",
"status": "Shipped",
"total": 149.5
}
]
}
]
}
}
}Default remapping
The default remapper converts every cell to the format for its field kind. It can convert numeric strings, boolean inputs, and date inputs.
import { defineResource, fields, table } from '@backlit/sdk'
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
fields.boolean('paid', {
label: 'Paid',
displayVariant: 'badge',
}),
fields.date('placedAt', {
label: 'Placed',
displayVariant: 'date',
}),
])
orders.view('list', () =>
table([
{
reference: 'ORD-1042',
total: '149.50',
paid: 1,
placedAt: 1_775_987_400_000,
},
]).setFields(
orders.pick('reference', 'total', 'paid', 'placedAt')
)
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
},
{
"kind": "boolean",
"name": "paid",
"label": "Paid",
"displayVariant": "badge"
},
{
"kind": "date",
"name": "placedAt",
"label": "Placed",
"displayVariant": "date"
}
],
"data": [
{
"reference": "ORD-1042",
"total": 149.5,
"paid": true,
"placedAt": "2026-04-12T09:50:00.000Z"
}
]
}
]
}
}
}Footer
Use withFooter for totals and other summaries. Put each value under the field
that it summarizes. The renderer puts the label in the columns before the first
footer value.
The first table field cannot contain a footer value because the footer label needs at least one column.
{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "detail",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "item",
"label": "Item"
},
{
"kind": "number",
"name": "quantity",
"format": "integer",
"label": "Qty"
},
{
"kind": "number",
"name": "unitPrice",
"format": "currency",
"currency": "USD",
"label": "Unit price"
},
{
"kind": "number",
"name": "lineTotal",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"item": "Desk lamp",
"quantity": 2,
"unitPrice": 49.5,
"lineTotal": 99
},
{
"item": "LED bulb",
"quantity": 4,
"unitPrice": 8.5,
"lineTotal": 34
}
],
"footer": [
{
"label": "Total",
"values": {
"quantity": 6,
"lineTotal": 133
}
}
]
}
]
}
}
}Multiple footer rows
Pass multiple footer rows for a subtotal, tax, total, or another sequence of summaries. A footer row can contain values for more than one field.
{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "detail",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "item",
"label": "Item"
},
{
"kind": "number",
"name": "quantity",
"format": "integer",
"label": "Qty"
},
{
"kind": "number",
"name": "unitPrice",
"format": "currency",
"currency": "USD",
"label": "Unit price"
},
{
"kind": "number",
"name": "lineTotal",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"item": "Desk lamp",
"quantity": 2,
"unitPrice": 49.5,
"lineTotal": 99
},
{
"item": "LED bulb",
"quantity": 4,
"unitPrice": 8.5,
"lineTotal": 34
}
],
"footer": [
{
"label": "Subtotal",
"values": {
"lineTotal": 133
}
},
{
"label": "Tax",
"values": {
"lineTotal": 26.6
}
},
{
"label": "Total",
"values": {
"quantity": 6,
"lineTotal": 159.6
}
}
]
}
]
}
}
}Global remapper
Use remappers.registerRemappers when an adapter uses a different value format.
The change applies to the role in all blocks. A table does not accept local
remapper overrides.
import {
defineResource,
fields,
remappers,
table,
} from '@backlit/sdk'
remappers.registerRemappers('display', {
date: (value) =>
typeof value === 'number'
? new Date(value * 1000).toISOString()
: null,
})
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.date('placedAt', {
label: 'Placed',
displayVariant: 'date',
}),
])
orders.view('list', () =>
table([
{ reference: 'ORD-1042', placedAt: 1_775_987_400 },
{ reference: 'ORD-1043', placedAt: 1_776_073_800 },
]).setFields(orders.pick('reference', 'placedAt'))
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "date",
"name": "placedAt",
"label": "Placed",
"displayVariant": "date"
}
],
"data": [
{
"reference": "ORD-1042",
"placedAt": "2026-04-12T09:50:00.000Z"
},
{
"reference": "ORD-1043",
"placedAt": "2026-04-13T09:50:00.000Z"
}
]
}
]
}
}
}Defect reporter
A defect occurs when a cell is missing or its value cannot be remapped. The SDK leaves that cell out of the row and includes its row index in the defect. If you do not set a reporter, the SDK writes a warning to the console.
import type { Defect } from '@backlit/sdk/types'
import { defineResource, fields, table } from '@backlit/sdk'
const defects = new Set<Defect>()
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view('list', () =>
table([
{ reference: 'ORD-1042', total: 149.5 },
{ reference: 'ORD-1043', total: 'not available' },
])
.setFields(orders.pick('reference', 'total'))
.onDefect((defect) => defects.add(defect))
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"reference": "ORD-1042",
"total": 149.5
},
{
"reference": "ORD-1043"
}
]
}
]
}
}
}All options
You can use footer rows and a defect reporter on the same table.
import type { Defect } from '@backlit/sdk/types'
import { defineResource, fields, table } from '@backlit/sdk'
const defects = new Set<Defect>()
const orders = defineResource('orders', [
fields.text('reference', { label: 'Order' }),
fields.date('placedAt', {
label: 'Placed',
displayVariant: 'date',
}),
fields.number('total', {
label: 'Total',
format: 'currency',
currency: 'USD',
}),
])
orders.view('list', () =>
table([
{
reference: 'ORD-1042',
placedAt: 1_775_987_400,
total: '149.50',
},
{
reference: 'ORD-1043',
placedAt: 1_776_073_800,
total: '89.00',
},
])
.setFields(
orders.pick('reference', 'placedAt', 'total')
)
.withFooter([
{ label: 'Total', values: { total: '238.50' } },
])
.onDefect((defect) => defects.add(defect))
)
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "reference",
"label": "Order"
},
{
"kind": "date",
"name": "placedAt",
"label": "Placed",
"displayVariant": "date"
},
{
"kind": "number",
"name": "total",
"format": "currency",
"currency": "USD",
"label": "Total"
}
],
"data": [
{
"reference": "ORD-1042",
"placedAt": "1970-01-21T13:19:47.400Z",
"total": 149.5
},
{
"reference": "ORD-1043",
"placedAt": "1970-01-21T13:21:13.800Z",
"total": 89
}
],
"footer": [
{
"label": "Total",
"values": {
"total": 238.5
}
}
]
}
]
}
}
}Field roles
setRoles({ fieldName: role }) selects roles for individual fields and replaces
the previous selections. The default role is display in tables and datalists,
and input in form field blocks. See Field roles.
Row controls and search state
Use onRowClick(linkTo(view, row => [row.id])) to make each row a link.
Use setRowActions for standalone visit or action controls. See
Targets. Use setPaginator and setSorter to place state
bindings from one view or zone scope. The resolver must apply the parsed state
to the data query. See Search state.