Skip to content
API 风格:

Table ​

Column-config table with sorting, fixed columns, stripes, cell slots, and virtual scrolling for large datasets (window computed by pure core functions, no external library).

Basic usage ​

Basic
Name
Role
City
Shen Yan
Architect
Hangzhou
Su Wan
Frontend
Suzhou
Lu Li
Designer
Beijing
vue
<script setup>
const cols = [
  { prop: 'name', label: 'Name', width: 140 },
  { prop: 'role', label: 'Role' },
  { prop: 'city', label: 'City', width: 120 },
]
const rows = [
  { id: 1, name: 'Shen Yan', role: 'Architect', city: 'Hangzhou' },
  { id: 2, name: 'Su Wan', role: 'Frontend', city: 'Suzhou' },
]
</script>

<template>
  <LhTable :data="rows" :columns="cols" />
</template>
vue
<script>
export default {
  data() {
    return {
      cols: [
        { prop: 'name', label: 'Name', width: 140 },
        { prop: 'role', label: 'Role' },
        { prop: 'city', label: 'City', width: 120 },
      ],
      rows: [
        { id: 1, name: 'Shen Yan', role: 'Architect', city: 'Hangzhou' },
        { id: 2, name: 'Su Wan', role: 'Frontend', city: 'Suzhou' },
      ],
    }
  },
}
</script>

<template>
  <LhTable :data="rows" :columns="cols" />
</template>

Sorting and fixed columns ​

Sort + fixed + stripe
Name
Role
City
Shen Yan
34
Architect
Hangzhou
Su Wan
28
Frontend
Suzhou
Lu Li
31
Designer
Beijing
vue
<script setup lang="ts">
const sortCols = [
  { prop: 'name', label: 'Name', width: 140, fixed: 'left' },
  { prop: 'age', label: 'Age', width: 100, sortable: true, align: 'center' },
  { prop: 'role', label: 'Role' },
  { prop: 'city', label: 'City', width: 120 },
]
const rows = [
  { id: 1, name: 'Shen Yan', age: 34, role: 'Architect', city: 'Hangzhou' },
  { id: 2, name: 'Su Wan', age: 28, role: 'Frontend', city: 'Suzhou' },
]
</script>

<template>
  <LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template>
vue
<script>
export default {
  data() {
    return {
      sortCols: [
        { prop: 'name', label: 'Name', width: 140, fixed: 'left' },
        { prop: 'age', label: 'Age', width: 100, sortable: true, align: 'center' },
        { prop: 'role', label: 'Role' },
        { prop: 'city', label: 'City', width: 120 },
      ],
      rows: [
        { id: 1, name: 'Shen Yan', age: 34, role: 'Architect', city: 'Hangzhou' },
        { id: 2, name: 'Su Wan', age: 28, role: 'Frontend', city: 'Suzhou' },
      ],
    }
  },
}
</script>

<template>
  <LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template>

Virtual scrolling ​

Setting height enables it: only rows inside the visible window are rendered. rowHeight must match the real row height.

Virtual 1000 rows
Name
Role
City
Row 1
Architect
Hangzhou
Row 2
Frontend
Hangzhou
Row 3
Architect
Hangzhou
Row 4
Frontend
Hangzhou
Row 5
Architect
Hangzhou
Row 6
Frontend
Hangzhou
vue
<script setup lang="ts">
const cols = [
  { prop: 'name', label: 'Name', width: 140 },
  { prop: 'role', label: 'Role' },
  { prop: 'city', label: 'City', width: 120 },
]

const bigRows = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  name: `Row ${i + 1}`,
  role: i % 2 === 0 ? 'Architect' : 'Frontend',
  city: 'Hangzhou',
}))
</script>

<template>
  <LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template>
vue
<script>
export default {
  data() {
    return {
      cols: [
        { prop: 'name', label: 'Name', width: 140 },
        { prop: 'role', label: 'Role' },
        { prop: 'city', label: 'City', width: 120 },
      ],
      bigRows: Array.from({ length: 1000 }, (_, i) => ({
        id: i + 1,
        name: `Row ${i + 1}`,
        role: i % 2 === 0 ? 'Architect' : 'Frontend',
        city: 'Hangzhou',
      })),
    }
  },
}
</script>

<template>
  <LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template>

Multi-level headers ​

Nest children in the column config to build grouped headers (arbitrary depth); the body aligns to leaf columns.

Two-level header
Name
Profile
Role
City
Shen Yan
Architect
Hangzhou
34
Su Wan
Frontend
Suzhou
28
Lu Li
Designer
Beijing
31
vue
<script setup>
const nestedCols = [
  { prop: 'name', label: 'Name', width: 140, fixed: 'left' },
  {
    label: 'Profile',
    children: [
      { prop: 'role', label: 'Role' },
      { prop: 'city', label: 'City', width: 120 },
    ],
  },
  { prop: 'age', label: 'Age', sortable: true, align: 'center' },
]
</script>

<template>
  <LhTable :data="rows" :columns="nestedCols" border />
</template>
vue
<script>
export default {
  data() {
    return {
      nestedCols: [
        { prop: 'name', label: 'Name', width: 140, fixed: 'left' },
        {
          label: 'Profile',
          children: [
            { prop: 'role', label: 'Role' },
            { prop: 'city', label: 'City', width: 120 },
          ],
        },
        { prop: 'age', label: 'Age', sortable: true, align: 'center' },
      ],
    }
  },
}
</script>

<template>
  <LhTable :data="rows" :columns="nestedCols" border />
</template>

Filtering ​

Configure filters on a column to add a filter trigger in its header; check options then press Confirm / Reset. filterMultiple: false switches to single choice, filterMethod customizes the predicate, controlled usage below.

Column filter (controlled)
Name
Role
Shen Yan
Architect
Su Wan
Frontend
Lu Li
Designer

Active filters: {}

vue
<script setup>
import { ref } from 'vue'

const filteredValues = ref({})

const filterCols = [
  { prop: 'name', label: 'Name', width: 140 },
  {
    prop: 'role', label: 'Role', filters: [
      { text: 'Architect', value: 'Architect' },
      { text: 'Frontend', value: 'Frontend' },
      { text: 'Designer', value: 'Designer' },
    ],
  },
]

function onFilterChange({ prop, values }) {
  console.log('filterChange', prop, values)
}
</script>

<template>
  <!-- Controlled filtering -->
  <LhTable
    :data="rows"
    :columns="filterCols"
    v-model:filtered-values="filteredValues"
    @filter-change="onFilterChange"
  />
</template>
vue
<script>
export default {
  data() {
    return {
      filteredValues: {},
      filterCols: [
        { prop: 'name', label: 'Name', width: 140 },
        {
          prop: 'role', label: 'Role', filters: [
            { text: 'Architect', value: 'Architect' },
            { text: 'Frontend', value: 'Frontend' },
            { text: 'Designer', value: 'Designer' },
          ],
        },
      ],
    }
  },
  methods: {
    onFilterChange({ prop, values }) {
      console.log('filterChange', prop, values)
    },
  },
}
</script>

<template>
  <!-- Controlled filtering -->
  <LhTable
    :data="rows"
    :columns="filterCols"
    v-model:filtered-values="filteredValues"
    @filter-change="onFilterChange"
  />
</template>

Column resizing ​

Enable resizable (global switch) and mark resizable: true on a leaf column with a numeric width. A drag handle appears at the column edge; when focused, ←/→ adjust by 10px, drag floor is 40px.

Resize columns
Name
Role
City
Shen Yan
Architect
Hangzhou
Su Wan
Frontend
Suzhou
Lu Li
Designer
Beijing

Last resize: (none)

vue
<script setup>
import { ref } from 'vue'

const resizeLog = ref('(none)')

const resizeCols = [
  { prop: 'name', label: 'Name', width: 140, resizable: true, fixed: 'left' },
  { prop: 'role', label: 'Role' },
  { prop: 'city', label: 'City', width: 120, resizable: true },
]

function onColumnResize({ prop, width }) {
  resizeLog.value = `${prop} → ${width}px`
}
</script>

<template>
  <!-- Column resizing -->
  <LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template>
vue
<script>
export default {
  data() {
    return {
      resizeLog: '(none)',
      resizeCols: [
        { prop: 'name', label: 'Name', width: 140, resizable: true, fixed: 'left' },
        { prop: 'role', label: 'Role' },
        { prop: 'city', label: 'City', width: 120, resizable: true },
      ],
    }
  },
  methods: {
    onColumnResize({ prop, width }) {
      this.resizeLog = `${prop} → ${width}px`
    },
  },
}
</script>

<template>
  <!-- Column resizing -->
  <LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template>

Row key, max height and default sort ​

row-key specifies the unique row key (recommended); max-height makes the header sticky with a scrollable body; sorter customizes sorting; default-sort-prop / default-sort-order set the initial sort; default-filtered-values sets initial filters.

rowKey / maxHeight / default sort
Name
Role
Shen Yan
34
Architect
Lu Li
31
Designer
Su Wan
28
Frontend
vue
<script setup>
const sorterCols = [
  { prop: 'name', label: 'Name', width: 140 },
  { prop: 'age', label: 'Age', sortable: true, sorter: (a, b) => a.age - b.age },
  { prop: 'role', label: 'Role' },
]
</script>

<template>
  <LhTable
    :data="rows"
    :columns="sorterCols"
    row-key="name"
    max-height="200"
    default-sort-prop="age"
    default-sort-order="desc"
  />
</template>
vue
<script>
export default {
  data() {
    return {
      sorterCols: [
        { prop: 'name', label: 'Name', width: 140 },
        { prop: 'age', label: 'Age', sortable: true, sorter: (a, b) => a.age - b.age },
        { prop: 'role', label: 'Role' },
      ],
    }
  },
}
</script>

<template>
  <LhTable
    :data="rows"
    :columns="sorterCols"
    row-key="name"
    max-height="200"
    default-sort-prop="age"
    default-sort-order="desc"
  />
</template>

API ​

LhTable Props ​

AttributeDescriptionTypeDefault
dataRow data[LhTableRow](#type-LhTableRow)[]—
columnsColumn config[LhTableColumn](#type-LhTableColumn)[]—
heightBody height (enables virtual scrolling; number is px)string | number—
maxHeightMax height in non-virtual modestring | number—
rowHeightVirtual row height (must match the real row height)number[LH_TABLE_ROW_HEIGHT](#const-LH_TABLE_ROW_HEIGHT)
stripeZebra stripesbooleanfalse
borderCell bordersbooleanfalse
sizeSize"small" | "default" | "large"'default'
rowKeyRow key field namestring'id'
defaultSortPropDefault sort columnstring—
defaultSortOrderDefault sort order"asc" | "desc"—
sorterCustom sorter(a: LhTableRow, b: LhTableRow) => number—
resizableEnable column resizing (also needs column-level resizable)booleanfalse
defaultFilteredValuesDefault filter state (uncontrolled, keyed by column prop){ [x: string]: (string | number)[]; }() => ({})
filteredValuesControlled filter state (keyed by column prop){ [x: string]: (string | number)[]; }—
sxQuick layout propsLhSxProps—

LhTable Events ​

EventDescriptionCallback
sortChangeSorting changed by clicking a sortable header(payload: { prop: string; order: 'asc' | 'desc' | undefined }) => void
rowClickA body row is clicked(row: LhTableRow, index: number) => void
filterChangeFilter options confirmed on a column(payload: { prop: string; values: (string | number)[] }) => void
update:filteredValuesFilter state changed (v-model:filteredValues)(values: Record<string, (string | number)[]>) => void
columnResizeColumn width changed by dragging the resize handle(payload: { prop: string; width: number }) => void

Slots: empty, <column.slot> (scope { row, column, index, value }).

LhTableColumn ​

FieldDescriptionType
propData field namestring
labelColumn titlestring
widthColumn width (number is px; evenly split when omitted)number | string
alignAlignment'left' | 'center' | 'right'
fixedFixed column (requires width)'left' | 'right'
sortableSortableboolean
slotCell slot namestring
formatterValue formatter(row, value) => string
childrenChild columns (nesting builds grouped headers)LhTableColumn[]
filtersFilter options (leaf columns only)LhTableFilterOption[]
filterMethodCustom filter predicate (defaults to strict prop equality)(value, row) => boolean
filterMultipleWhether the filter panel allows multiple choicesboolean
resizableColumn is resizable (numeric-width leaf column only)boolean

Accessibility ​

  • role="grid" with aria-rowcount / aria-colcount; rows, columnheaders and gridcells
  • Sortable headers expose aria-sort (none / ascending / descending), triggered by a real <button>
  • Fixed columns use CSS position: sticky (no hand-rolled scroll math)
  • Filter triggers expose aria-haspopup + aria-expanded; the panel is role="group"
  • Resize handles are role="separator", focusable and adjustable with ←/→ in 10px steps

Type Definitions ​

The TypeScript type structures exported by this component (auto-extracted from packages/components/table/src/types.ts and @lh-ui/share/core/* sources; click each entry to expand; see the Props table above for attribute descriptions):

LhTableProps
ts
export interface LhTableProps {
  data: LhTableRow[]
  columns: LhTableColumn[]
  height?: number | string
  maxHeight?: number | string
  rowHeight?: number
  stripe?: boolean
  border?: boolean
  size?: 'small' | 'default' | 'large'
  rowKey?: string
  defaultSortProp?: string
  defaultSortOrder?: LhTableSortOrder
  sorter?: (a: LhTableRow, b: LhTableRow) => number
  resizable?: boolean
  defaultFilteredValues?: Record<string, (string | number)[]>
  filteredValues?: Record<string, (string | number)[]>
  sx?: LhSxProps
}
LhTableSortOrder (from @lh-ui/share/core/table)
ts
export type LhTableSortOrder = 'asc' | 'desc'
LhTableRow (from @lh-ui/share/core/table)
ts
export type LhTableRow = Record<string, any>
LhTableColumn (from @lh-ui/share/core/table)
ts
export interface LhTableColumn {
  prop: string
  label: string
  width?: number | string
  align?: LhTableAlign
  fixed?: 'left' | 'right'
  sortable?: boolean
  slot?: string
  formatter?: (row: LhTableRow, value: any) => string
  children?: LhTableColumn[]
  filters?: LhTableFilterOption[]
  filterMethod?: (value: string | number, row: LhTableRow) => boolean
  filterMultiple?: boolean
  resizable?: boolean
}
LhTableFilterOption (from @lh-ui/share/core/table)
ts
export interface LhTableFilterOption {
  text: string
  value: string | number
}
LhTableAlign (from @lh-ui/share/core/table)
ts
export type LhTableAlign = 'left' | 'center' | 'right'

Constants ​

Public constants used by this component (linked from the Type / Default columns in the Props table above):

LH_TABLE_ROW_HEIGHT (from @lh-ui/share/core/table)
ts
export const LH_TABLE_ROW_HEIGHT = 48
LH_TABLE_OVERSCAN (from @lh-ui/share/core/table)
ts
export const LH_TABLE_OVERSCAN = 3
LH_TABLE_MIN_COL_WIDTH (from @lh-ui/share/core/table)
ts

export const LH_TABLE_MIN_COL_WIDTH = 40

Beauty of tradition, foundation of code