Data Table

Search, sort, paginate, and customize structured dashboard data.

DataTable is a dashboard composition built on Table. It accepts plain column definitions and rows, then provides filtering, sortable columns, pagination, loading and empty states, and custom cell rendering without a table engine dependency. It processes rows client-side by default and supports remote data through manual mode.

Workspace members
Status
Ada Lovelaceada@example.comAdminActive
Grace Hoppergrace@example.comEditorActive
Katherine Johnsonkatherine@example.comAnalystActive
Margaret Hamiltonmargaret@example.comEngineerInvited
Dorothy Vaughandorothy@example.comManagerActive
1–5 of 6 rows
vue
<script setup lang="ts">
import { Badge, Button, DataTable } from '@typlog/ui'
import type { DataTableColumn } from '@typlog/ui'

type Member = {
  id: number
  name: string
  email: string
  role: string
  status: 'Active' | 'Invited'
}

const columns: DataTableColumn<Member>[] = [
  { key: 'name', label: 'Member', sortable: true },
  { key: 'email', label: 'Email', sortable: true },
  { key: 'role', label: 'Role', sortable: true },
  { key: 'status', label: 'Status' },
]

const members: Member[] = [
  { id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'Admin', status: 'Active' },
  { id: 2, name: 'Grace Hopper', email: 'grace@example.com', role: 'Editor', status: 'Active' },
  { id: 3, name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Analyst', status: 'Active' },
  { id: 4, name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Engineer', status: 'Invited' },
  { id: 5, name: 'Dorothy Vaughan', email: 'dorothy@example.com', role: 'Manager', status: 'Active' },
  { id: 6, name: 'Mary Jackson', email: 'mary@example.com', role: 'Engineer', status: 'Invited' },
]
</script>

<template>
  <DataTable
    caption="Workspace members"
    :columns="columns"
    :rows="members"
    row-key="id"
    :page-size="5"
  >
    <template #toolbar>
      <Button size="2">Invite member</Button>
    </template>
    <template #cell-status="{ value }">
      <Badge :color="value === 'Active' ? 'green' : 'amber'">
        {{ value }}
      </Badge>
    </template>
  </DataTable>
</template>

Use a cell-{key} slot to customize a column. Each cell slot receives the original row, its column, and the resolved value. The toolbar slot is rendered beside the built-in search field. For server-side state or advanced features such as grouping and virtualization, compose the lower-level Table component with the table engine used by the application.

Column definitions ​

PropertyTypeDescription
keystringStable column identifier and fallback row property.
labelstringVisible column heading.
accessor(row) => unknownResolves computed or nested values.
sortablebooleanEnables the three-state sort button.
searchablebooleanIncludes the value in text filtering unless set to false.
alignstart | center | endAligns the heading and cells.
widthstringSets the column width.

Loading data with fetch ​

Set manual when filtering, sorting, and pagination happen on the server. In manual mode, rows represents the current page and total is the total number of matching rows. DataTable emits request once after mounting and whenever the search, page, page size, or sort state changes.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable } from '@typlog/ui'
import type { DataTableColumn, DataTableRequest } from '@typlog/ui'

type Member = {
  id: number
  name: string
  role: string
}

type MemberPage = {
  rows: Member[]
  total: number
}

const columns: DataTableColumn<Member>[] = [
  { key: 'name', label: 'Member', sortable: true },
  { key: 'role', label: 'Role', sortable: true },
]
const rows = ref<Member[]>([])
const total = ref(0)
const loading = ref(false)

async function loadMembers(request: DataTableRequest) {
  const params = new URLSearchParams({
    page: String(request.page),
    pageSize: String(request.pageSize),
    search: request.search,
  })
  if (request.sort) {
    params.set('sort', request.sort.key)
    params.set('direction', request.sort.direction)
  }

  loading.value = true
  try {
    const response = await fetch(`/api/members?${params}`)
    if (!response.ok) throw new Error('Could not load members')

    const result: MemberPage = await response.json()
    rows.value = result.rows
    total.value = result.total
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <DataTable
    manual
    :columns="columns"
    :rows="rows"
    :total="total"
    :loading="loading"
    row-key="id"
    @request="loadMembers"
  />
</template>

The search field emits on every input change. Debounce loadMembers or cancel stale requests when the endpoint should not receive a request per keystroke.

Events ​

EventPayloadWhen it fires
update:searchstringThe built-in search field changes or is cleared.
update:pagenumberPagination changes or the current page is reset.
update:sortDataTableSort | nullA sortable heading changes or clears the sort.
requestDataTableRequestManual mode mounts or its query state changes.

Use v-model:search, v-model:page, and v-model:sort when the parent also needs to control those values. They are optional when @request is sufficient.

States ​

Loading reports
ReportOwner
Loading rows…
Saved reports
ReportOwner
No saved reports yet.
vue
<script setup lang="ts">
import { DataTable } from '@typlog/ui'
import type { DataTableColumn } from '@typlog/ui'

type Report = {
  name: string
  owner: string
}

const columns: DataTableColumn<Report>[] = [
  { key: 'name', label: 'Report' },
  { key: 'owner', label: 'Owner' },
]
</script>

<template>
  <div class="grid gap-4">
    <DataTable
      caption="Loading reports"
      :columns="columns"
      :rows="[]"
      loading
      :searchable="false"
    />
    <DataTable
      caption="Saved reports"
      :columns="columns"
      :rows="[]"
      empty-text="No saved reports yet."
      :searchable="false"
    />
  </div>
</template>

API Reference ​

PropDefaultType
caption
–
string

Accessible table caption.

columns
–
readonly DataTableColumn<Row>[]

Column definitions in display order.

emptyText"No results found."
string

Message displayed when no rows match.

layout"auto"
"auto""fixed"

Controls the native table layout algorithm.

loadingfalse
boolean

Displays the loading state instead of rows.

loadingText"Loading rows…"
string

Message displayed while rows are loading.

manualfalse
boolean

Disables client-side filtering, sorting, and pagination. In manual mode, rows should contain the current page and request drives data loading.

page
–
number

Controlled current page. Can be bound with v-model:page.

pageSize10
number

Number of rows displayed on each page.

rowKey
–
Enum

Property or function that returns a stable key for each row.

rows
–
readonly Row[]

Rows displayed by the table.

search
–
string

Controlled text filter. Can be bound with v-model:search.

searchabletrue
boolean

Shows the built-in text filter.

searchLabel"Search rows"
string

Accessible label for the text filter.

searchPlaceholder"Filter rows…"
string

Placeholder displayed by the text filter.

size"2"
"1""2""3"

Controls table density.

sort
–
DataTableSortnull

Controlled sort state. Can be bound with v-model:sort.

total
–
number

Total row count used for pagination in manual mode.

variant"surface"
"ghost""surface"

Controls the table surface treatment.

Last updated Sep 15, 2026