DataTable
DataTable is a table for reading and comparing rows. Use it for directories, search results, and audit logs where native table semantics and row selection matter. Choose DataGrid when every cell must support keyboard navigation and editing.
Search, sorting, and selection share one projection.<script setup lang="ts">
import {
createDataTableComponents,
useDataTable,
} from '@sectile/vue/data-table'
const table = useDataTable({
source: resolveUsers,
})
const DataTable = createDataTableComponents(table)
</script>
<template>
<DataTable.Provider>
<DataTable.Root>
<DataTable.Caption>사용자</DataTable.Caption>
<DataTable.Header><DataTable.HeaderRow>
<DataTable.ColumnHeader column="name">
<DataTable.SortTrigger column="name">이름</DataTable.SortTrigger>
</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="team">팀</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="status">상태</DataTable.ColumnHeader>
</DataTable.HeaderRow></DataTable.Header>
<DataTable.Body v-slot="{ row }">
<DataTable.Cell column="name">{{ row.cells.name }}</DataTable.Cell>
<DataTable.Cell column="team">{{ row.cells.team }}</DataTable.Cell>
<DataTable.Cell column="status">{{ row.cells.status }}</DataTable.Cell>
</DataTable.Body>
</DataTable.Root>
</DataTable.Provider>
</template>The Code tab switches the same UI between Vue compound components, DOM bindings for existing HTML, and renderer-free Core APIs.
Search and sort
Repeatedly activate a heading to cycle ascending, descending, and off. Search and sort update one query and request a new view instead of mutating mounted DOM rows. The same UI therefore works with both in-memory and server-backed data.
{"sort":[],"filters":[]}<script setup lang="ts">
import {
createDataTableComponents,
useDataTable,
} from '@sectile/vue/data-table'
const table = useDataTable({
source: resolveUsers,
})
const DataTable = createDataTableComponents(table)
</script>
<template>
<DataTable.Provider>
<DataTable.Root>
<DataTable.Caption>사용자</DataTable.Caption>
<DataTable.FilterControl
scope="global" id="search" predicate="contains"
placeholder="이름 또는 팀 검색"
/>
<DataTable.Header><DataTable.HeaderRow>
<DataTable.ColumnHeader column="name">
<DataTable.SortTrigger column="name">이름</DataTable.SortTrigger>
</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="team">팀</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="status">상태</DataTable.ColumnHeader>
</DataTable.HeaderRow></DataTable.Header>
<DataTable.Body v-slot="{ row }">
<DataTable.Cell column="name">{{ row.cells.name }}</DataTable.Cell>
<DataTable.Cell column="team">{{ row.cells.team }}</DataTable.Cell>
<DataTable.Cell column="status">{{ row.cells.status }}</DataTable.Cell>
</DataTable.Body>
</DataTable.Root>
</DataTable.Provider>
</template>SortTriggerrecords a column and comparator in the query.FilterControlrecords a global or column filter.- A client source evaluates that query locally; a remote source serializes it for HTTP or RPC.
See async data sources for a complete request, cancellation, failure, and retry flow.
Select rows and all matching results
Individual checkboxes, Shift ranges, and selecting every row matched by the current query share one selection contract. The header checkbox exposes false, mixed, and true for none, some, and all matching rows.
{"kind":"explicit-rows","rowIDs":[]}<script setup lang="ts">
import {
createDataTableComponents,
useDataTable,
} from '@sectile/vue/data-table'
const table = useDataTable({
source: resolveUsers,
})
const DataTable = createDataTableComponents(table)
</script>
<template>
<DataTable.Provider>
<DataTable.Root>
<DataTable.Caption>사용자</DataTable.Caption>
<DataTable.Header><DataTable.HeaderRow>
<th><DataTable.BulkSelectionControl :target="{ kind: 'all-matching' }" /></th>
<DataTable.ColumnHeader column="name">이름</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="team">팀</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="status">상태</DataTable.ColumnHeader>
</DataTable.HeaderRow></DataTable.Header>
<DataTable.Body v-slot="{ row }">
<td><DataTable.SelectionControl name="selected-users" /></td>
<DataTable.Cell column="name">{{ row.cells.name }}</DataTable.Cell>
<DataTable.Cell column="team">{{ row.cells.team }}</DataTable.Cell>
<DataTable.Cell column="status">{{ row.cells.status }}</DataTable.Cell>
</DataTable.Body>
</DataTable.Root>
</DataTable.Provider>
</template>SelectionControl inherits the current Body row. BulkSelectionControl with all-matching stores the query revision and exclusions rather than every unloaded ID. Set name for native form submission and override value only when it differs from the row ID.
Multi-level headers and edit intent
Do not specify a header-row depth. Bind leaf headers with column, and only bind a spanning group with header. Tabular derives depth, colspan, rowspan, and accessibility metadata from the schema.
Two header levels · native input commit intent<script setup lang="ts">
import {
createDataTableComponents,
useDataTable,
} from '@sectile/vue/data-table'
const table = useDataTable({
source: resolveUsers,
})
const DataTable = createDataTableComponents(table)
</script>
<template>
<DataTable.Provider>
<DataTable.Root>
<DataTable.Caption>사용자</DataTable.Caption>
<DataTable.Header>
<DataTable.HeaderRow>
<DataTable.ColumnHeader header="identity">사용자 정보</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="status">상태</DataTable.ColumnHeader>
</DataTable.HeaderRow>
<DataTable.HeaderRow>
<DataTable.ColumnHeader column="name">이름</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="team">팀</DataTable.ColumnHeader>
</DataTable.HeaderRow>
</DataTable.Header>
<DataTable.Body v-slot="{ row }">
<DataTable.Cell column="name">
<DataTable.Editor as-child column="name"><input :value="row.cells.name"></DataTable.Editor>
</DataTable.Cell>
<DataTable.Cell column="team">{{ row.cells.team }}</DataTable.Cell>
<DataTable.Cell column="status">{{ row.cells.status }}</DataTable.Cell>
</DataTable.Body>
</DataTable.Root>
</DataTable.Provider>
</template>Editor does not persist data. It converts a native input commit into a typed command; the application owns validation, storage, and optimistic updates. Use DataGrid when a cell cursor and edit mode are central.
Column visibility, pinning, and size
Order, visibility, and start/end pinning are portable semantic state. Pixel width belongs to the DOM or Vue host. This keeps Core platform-independent while each rendered surface can use real measurements.
{"hidden":[],"pinnedStart":[]}<script setup lang="ts">
import {
createDataTableComponents,
useDataTable,
} from '@sectile/vue/data-table'
const table = useDataTable({
source: resolveUsers,
})
const DataTable = createDataTableComponents(table)
</script>
<template>
<DataTable.Provider>
<DataTable.Root>
<DataTable.Caption>사용자</DataTable.Caption>
<DataTable.Header><DataTable.HeaderRow>
<DataTable.ColumnHeader column="name">
이름
<DataTable.ColumnResizeHandle column="name" :min-size="160" :max-size="480" />
</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="team">팀</DataTable.ColumnHeader>
<DataTable.ColumnHeader column="status">상태</DataTable.ColumnHeader>
</DataTable.HeaderRow></DataTable.Header>
<DataTable.Body v-slot="{ row }">
<DataTable.Cell column="name">{{ row.cells.name }}</DataTable.Cell>
<DataTable.Cell column="team">{{ row.cells.team }}</DataTable.Cell>
<DataTable.Cell column="status">{{ row.cells.status }}</DataTable.Cell>
</DataTable.Body>
</DataTable.Root>
</DataTable.Provider>
</template>ColumnResizeHandle supports pointer and keyboard input and respects min/max limits. Column visibility and pinning update controller columnState, so they can be persisted or controlled by the application.
Connect data and render request states
useDataTable({ source }) executes requests, and the returned controller exposes status, error, reload, and cancel. Tabular does not prescribe the spinner, empty state, error copy, or retry control. It keeps the last accepted view while exposing the new request separately.
| Situation | Read this state |
|---|---|
| Initial request | status === 'loading' with no accepted view |
| Sorting request | Existing rows remain while a request is pending |
| Empty result | Accepted view has rows.length === 0 |
| Failure and retry | error, reload() |
See async data sources for the complete flow.
Preserve row types in Vue
createDataTableComponents(table) creates a component namespace bound to the source response schema. A Body slot therefore preserves the inferred rows[].cells type, and Cell inherits the row ID from Body.
<DataTable.Body v-slot="{ row }">
<DataTable.Cell column="name">{{ row.cells.name }}</DataTable.Cell>
<DataTable.Cell column="role">{{ row.cells.role }}</DataTable.Cell>
</DataTable.Body>Body owns normal row iteration. Use <DataTable.Body manual> and explicit DataTable.Row only for low-level rendering such as a virtual window.
Find a public part
| Goal | Vue part | DOM/Core equivalent |
|---|---|---|
| Table and name | Root, Caption | native table / controller projection |
| Headers | Header, HeaderRow, ColumnHeader | header attributes / schema |
| Query | SortTrigger, FilterControl | bind functions / set-query event |
| Rows | Body, Row, Cell | element registration / projection rows |
| Selection | SelectionControl, BulkSelectionControl | checkbox binding / selection event |
| Groups | Disclosure | disclosure binding / expansion event |
| Editing | Editor | editor binding / commit command |
| Column size | ColumnResizeHandle | resize binding / host size state |
See Vue composition, DOM composition, and shared contracts for installation paths and full state shapes.
