SwTable

SwTable is an admin-console data table: the columns config drives the header and column order, cells render item[key] by default, and complex content (thumbnails / status badges / action button groups) goes through the #cell-<key> scoped slot; it ships the admin-console big three — controlled column sorting (v-model:sort), row selection for batch actions (v-model:selected), and a loading transition — plus stripes / density / sticky-header visual knobs. Visuals follow the shadcn Table baseline.

It outputs a semantic <table> (SEO / a11y), wrapped in a .sw-table container with overflow-x: auto — on narrow screens the table scrolls horizontally by itself instead of blowing up the page layout. Baseline styles (border + radius / muted header / row hover / cell padding) go through --sw-table-* tokens; override the variables to reskin.

Boundaries: pagination / filtering are not built in — compose SwPagination / SwSegmented / SwSelect at the page layer; sorting manages the interaction, not the data (controlled — see below); heavy features (pinned columns / resizable columns / tree rows / virtual scrolling) wait for real data points. This aligns with the framework's "layout only, no business UI" rule.

Basic usage

Basic columns (default cells read item[key])

加载中...
<script setup lang="ts">
const COLUMNS = [
  { key: 'name', label: '名称' },
  { key: 'category', label: '分类' },
  { key: 'count', label: '集数', width: '4rem', align: 'center' },
]
const rows = [{ id: 1, name: '未来都市', category: '动画', count: 12 }]
</script>

<template>
  <SwTable :columns="COLUMNS" :items="rows" />
</template>

Cell / header slots (the admin-list shape)

#cell-<key> customizes cells per column (scope { item, value, index }); #header-<key> customizes headers (scope { column }):

Status badge + inline actions (#cell-* slots)

加载中...

Empty placeholder

#empty renders as one full-width row when items is empty; omit it for an empty tbody:

#empty placeholder

加载中...

Column sorting (controlled)

column.sortable makes the header clickable, cycling asc → desc → none through v-model:sort (the header renders an indicator arrow + aria-sort). The component doesn't sort the data — the business sorts locally (toSorted) or re-fetches with params (server-side sorting is the admin-console mainstream):

Column sorting (controlled tri-state; this demo sorts locally)

加载中...
<script setup lang="ts">
const sort = ref<SwTableSort | undefined>()
// Server-side sorting: refetch with params on sort change
watch(sort, (s) => refetch({ orderBy: s?.key, order: s?.order }))
</script>

<template>
  <SwTable v-model:sort="sort" :columns="COLUMNS" :items="rows" />
</template>

Row selection & batch actions

selectable renders a checkbox column + select-all header (indeterminate when partially selected); the selection set goes through v-model:selected (an array of rowKey values):

Row selection + batch action

加载中...

Loading transition

loading dims the table and overlays a spinner while keeping the structure (no blank flash) — for "data present, refreshing" transitions; first-screen empty loading still belongs to the page layer's four states (skeletons). The two are complementary:

Page-flip transition (loading + SwPagination)

加载中...

Visual knobs: stripes & density

striped shades even rows; size="compact" tightens cell padding — a common combo for data-dense pages:

Stripes + compact density (toggle to compare)

加载中...

Sticky header (scrolling inside the container)

sticky-header + --sw-table-max-height turn the container into a scroll area with the header pinned on top:

Sticky header (30 rows scrolling in the container)

加载中...

The full admin-console shape

Sorting + row selection + loading + row click + status badges + inline actions combined (a real ops list page):

Admin console combined (all capabilities)

加载中...

Composing with pagination (page-layer data flow)

The table doesn't manage paging — use SwPagination to swap items per page:

<template>
  <SwTable :columns="COLUMNS" :items="pageItems">
    <template #empty>暂无数据</template>
  </SwTable>
  <SwPagination v-model:page="page" :total="total" :page-size="20" />
</template>

Props

NameTypeRequiredDefaultDescription
columnsSwTableColumn[]✓—Column config; each item is { key, label?, width?, align?, sortable? }
itemsT[]✓—Row data (generic; the component doesn't care about the shape)
rowKeystring | ((item: T) => string | number)'id'Row key field name or getter; falls back to index when unresolvable
stripedbooleanfalseZebra stripes (even rows shaded)
size'default' | 'compact''default'Density (compact tightens cell padding). ⚠️ Not the same as size on SwThemeSwitch / SwLangSwitch (an icon pixel value) — those are dimensions, this is a density enum, matching Ant / Element Table.size
stickyHeaderbooleanfalseSticky header (pair with --sw-table-max-height for an in-container scroll area)
loadingbooleanfalseRefresh transition (overlay + dimming, structure preserved)
selectablebooleanfalseRender a checkbox column + select-all header
sort (v-model:sort)SwTableSort | undefinedundefinedCurrent sort state; undefined = unsorted
selected (v-model:selected)(string | number)[][]Selection set of rowKey values

SwTableColumn: { key: string; label?: string; width?: string; align?: 'left' \| 'center' \| 'right'; sortable?: boolean } — key is both the default cell field and the #cell-<key> / #header-<key> slot-name suffix; width lands on <th> (a CSS width value); align defaults to 'left'; sortable makes the header clickable for sorting.

SwTableSort: { key: string; order: 'asc' \| 'desc' }. Both types are importable from the package: import type { SwTableColumn, SwTableSort } from '@swift-web/framework'.

Events

NamePayloadDescription
update:sortSwTableSort | undefinedFired on sortable header click (asc → desc → undefined cycle)
update:selected(string | number)[]Row / select-all selection changed
row-click(item, index)Fired on row click (clicks on inline interactive elements — button / a / input — don't trigger it); rows get cursor: pointer automatically when listened

Inline actions (edit / publish / delete) still use SwButton etc. inside #cell-<key> slots with their own events.

Slots

NameScopeDescription
cell-<key>{ item, value, index }Customize cells per column. Renders String(item[key] ?? '') when omitted
header-<key>{ column }Customize headers per column. Renders column.label when omitted. ⚠️ On sortable columns the slot content is wrapped in a <button> (for keyboard access), so it must be phrasing content — putting a <button> / <a> there creates nested interactive elements
empty—Rendered as one full-width row when items is empty (optional; empty tbody when omitted)

Styling Tokens

Brand the table via --sw-table-* tokens (fallbacks are the default look):

TokenDefaultApplies to
--sw-table-bgvar(--card)table background
--sw-table-bordervar(--border)outer border & row divider color
--sw-table-radiusvar(--radius)table radius
--sw-table-head-colorvar(--muted-foreground)header text
--sw-table-head-bgcolor-mix(in srgb, var(--foreground) 6%, transparent)header background (foreground overlay: distinguishable from rows in any theme)
--sw-table-hover-bgcolor-mix(in srgb, var(--primary) 4%, transparent)row hover background
--sw-table-striped-bgcolor-mix(in srgb, var(--foreground) 3%, transparent)zebra even-row background (striped)
--sw-table-cell-padding0.75rem 1remcell padding
--sw-table-cell-padding-compact0.375rem 0.75remcompact-density padding (size="compact")
--sw-table-font-sizeinherittable font size
--sw-table-max-heightnonecontainer max height (set a value for in-container scrolling, pair with stickyHeader)

Caveats

  • Sortable headers are real <button>s: on sortable columns the header content is wrapped in a <button> (the W3C ARIA APG Sortable Table pattern), so it is Tab-focusable and sorts on Enter / Space; aria-sort stays on the <th>. Target button.th-inner:hover to restyle the hover state.
  • row-click is a convenience layer, not the only path: rows are deliberately not made focusable (a long table would gain one Tab stop per row). Each row must contain a real focusable element performing the same action (a detail link or an action button); otherwise keyboard and screen-reader users cannot reach it at all — that fails WCAG 2.1.1 Keyboard (Level A), and mouse testing will never reveal it. A console.warn fires in dev; the check is stripped from production builds.
  • Sorting doesn't sort the data: v-model:sort is only the controlled interaction state — the business must sort itself (local toSorted or refetch with params), otherwise clicking headers "only flips the arrow, the data doesn't move".
  • loading vs. the four view states: loading serves the "data present, refreshing" transition; first-screen loading / error / empty still belong to the page layer's four states (skeletons / retry entries), with #empty as a light fallback row only.
  • Inline actions don't trigger row-click: clicks on button / a / input inside #cell-* don't bubble into a row click — no manual stopPropagation needed.
  • No built-in pagination / filtering: compose SwPagination / SwSegmented / SwSelect at the page layer.
  • Lock pending on inline async actions: action buttons inside #cell-* follow SB-001 (async @click with :loading), e.g. lock publish/unpublish by row id.
  • Horizontal scrolling on narrow screens is built in: the .sw-table container ships overflow-x: auto; with many columns just give them width for readable widths — don't wrap another scroll container outside.

See Also