Element UI Table Sorting, Filtering, and Pagination: A Complete Implementation Guide

Element UI tables implement sorting via the handleSortClick method in table-header.js, column filtering through lazy-loaded FilterPanel instances, and pagination as an independent component emitting size-change and current-change events for data slicing.

The ElemeFE/element repository provides a sophisticated Table component architecture that cleanly separates layout rendering from state management. By configuring the sortable and filters properties on el-table-column components and wiring the standalone el-pagination events to your data logic, you create responsive data grids that handle large datasets efficiently.

How Table Sorting Works

Column sorting in Element UI is handled internally by the table store, triggered from the header rendering logic. In packages/table/src/table-header.js, each column configured with sortable renders two caret icons (<i class="sort-caret …">) that capture click events.

When a user clicks a sort trigger, the handleSortClick method toggles the column's order property between ascending, descending, and null. This commits a changeSortCondition mutation to the table store, which updates sortProp and sortOrder states and reorders the row data before rendering.

To enable sorting, add the sortable prop to any column definition:

<template>
  <el-table :data="tableData" style="width: 100%">
    <el-table-column prop="date" label="Date" sortable width="180" />
    <el-table-column prop="name" label="Name" />
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { date: '2024-01-01', name: 'Alice' },
        { date: '2024-01-02', name: 'Bob' }
      ]
    };
  }
};
</script>

The table automatically handles string and number comparisons. For custom sorting logic, use the sort-method prop on the column to define a comparator function.

Implementing Column Filtering

Filtering utilizes a dynamic FilterPanel component that instantiates when users interact with the filter trigger. In packages/table/src/table-header.js, columns marked with filters render a clickable el-table__column-filter-trigger element.

Clicking this trigger lazily loads a FilterPanel Vue instance displaying a popover with checkboxes for each item in the column's filters array (an array of { text, value } objects). When selections change, the panel invokes the store's filterChange mutation, updating column.filteredValue and recomputing the displayed rows.

You must provide a filter-method function that determines whether a row should display for a given filter value:

<template>
  <el-table :data="tableData" style="width: 100%">
    <el-table-column
      prop="tag"
      label="Tag"
      :filters="[{ text: 'Home', value: 'Home' }, { text: 'Office', value: 'Office' }]"
      :filter-method="filterTag"
      filter-placement="bottom-end">
      <template slot-scope="scope">
        <el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'">
          {{ scope.row.tag }}
        </el-tag>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { tag: 'Home', address: '123 Main St' },
        { tag: 'Office', address: '456 Corporate Blvd' }
      ]
    };
  },
  methods: {
    filterTag(value, row) {
      return row.tag === value;
    }
  }
};
</script>

The filter-placement prop controls the popover positioning, accepting any valid Popper.js placement value.

Configuring Table Pagination

Unlike sorting and filtering, pagination is not built into the Table component. Instead, packages/pagination/src/pagination.js provides a standalone el-pagination component that emits events for you to slice your data source manually.

The pagination component accepts a layout prop defining which elements to display (e.g., "total, sizes, prev, pager, next, jumper"). It emits size-change when the user selects a new page size and current-change when navigating between pages.

For client-side pagination, maintain the full dataset in your component state and compute a sliced view:

<template>
  <div>
    <el-table :data="pagedData" style="width: 100%">
      <el-table-column prop="date" label="Date" />
      <el-table-column prop="name" label="Name" />
    </el-table>
    
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page.sync="currentPage"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :page-sizes="[5, 10, 20, 50]"
      :total="tableData.length">
    </el-pagination>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [ /* full dataset */ ],
      currentPage: 1,
      pageSize: 10
    };
  },
  computed: {
    pagedData() {
      const start = (this.currentPage - 1) * this.pageSize;
      return this.tableData.slice(start, start + this.pageSize);
    }
  },
  methods: {
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
    },
    handleCurrentChange(page) {
      this.currentPage = page;
    }
  }
};
</script>

For server-side pagination, replace the pagedData computed property with an API call triggered by the pagination events.

Complete Data Grid Implementation

Combining all three features requires careful sequencing: the Table component handles sorting automatically, but you must account for filtered row counts when configuring pagination. Access the table store's states.columns to retrieve active filter values for accurate total calculations.

<template>
  <div>
    <el-table 
      ref="dataTable"
      :data="pagedData" 
      style="width: 100%">
      <el-table-column prop="date" label="Date" sortable width="150" />
      <el-table-column
        prop="tag"
        label="Tag"
        :filters="tagFilters"
        :filter-method="filterTag"
        filter-placement="bottom-end">
        <template slot-scope="scope">
          <el-tag>{{ scope.row.tag }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="address" label="Address" />
    </el-table>

    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page.sync="currentPage"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next"
      :page-sizes="[5, 10, 20]"
      :total="filteredCount">
    </el-pagination>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [ /* full dataset */ ],
      currentPage: 1,
      pageSize: 10,
      tagFilters: [
        { text: 'Home', value: 'Home' },
        { text: 'Office', value: 'Office' }
      ]
    };
  },
  computed: {
    filteredCount() {
      // Access the table store to respect active filters in pagination total
      const table = this.$refs.dataTable;
      if (!table) return this.tableData.length;
      const tagColumn = table.store.states.columns.find(c => c.property === 'tag');
      if (!tagColumn || !tagColumn.filteredValue || !tagColumn.filteredValue.length) {
        return this.tableData.length;
      }
      return this.tableData.filter(row => tagColumn.filteredValue.includes(row.tag)).length;
    },
    filteredData() {
      const table = this.$refs.dataTable;
      if (!table) return this.tableData;
      const tagColumn = table.store.states.columns.find(c => c.property === 'tag');
      if (!tagColumn || !tagColumn.filteredValue || !tagColumn.filteredValue.length) {
        return this.tableData;
      }
      return this.tableData.filter(row => tagColumn.filteredValue.includes(row.tag));
    },
    pagedData() {
      const start = (this.currentPage - 1) * this.pageSize;
      return this.filteredData.slice(start, start + this.pageSize);
    }
  },
  methods: {
    filterTag(value, row) {
      return row.tag === value;
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
    },
    handleCurrentChange(page) {
      this.currentPage = page;
    }
  }
};
</script>

This implementation mirrors the official examples in examples/docs/en-US/table.md and examples/docs/en-US/pagination.md, ensuring compatibility with the Element UI store architecture.

Summary

  • Sorting relies on packages/table/src/table-header.js rendering sort carets that trigger handleSortClick, committing changeSortCondition mutations to reorder rows automatically.
  • Filtering uses lazy-loaded FilterPanel instances to update column.filteredValue via the filterChange store mutation, requiring a custom filter-method for matching logic.
  • Pagination operates independently in packages/pagination/src/pagination.js, emitting size-change and current-change events that you handle to slice the data array bound to the table.
  • For combined grids, reference the table store's states.columns to synchronize pagination totals with active filter states.

Frequently Asked Questions

How do I enable default sorting on table load?

Set the default-sort prop on the el-table component to an object specifying the column prop and initial order (either ascending or descending). For example: :default-sort="{ prop: 'date', order: 'descending' }". The table store initializes sortProp and sortOrder from this value before the first render.

Can I use server-side sorting and filtering instead of client-side?

Yes. Remove the sortable and filters props from columns to disable UI handlers, then implement custom header slots with clickable elements. Trigger your API calls within these handlers and update the data prop bound to the table with the server-response. Alternatively, set sortable="custom" and listen for the sort-change event to trigger remote requests while keeping the UI indicators.

Why does my pagination total not update when filters are applied?

The el-pagination component does not observe the table's internal filter state. You must manually calculate the filtered row count by accessing this.$refs.tableName.store.states.columns to check filteredValue arrays, or maintain a separate filtered dataset computed property that feeds both the table :data and pagination :total props.

How do I customize the sort icons in the table header?

Override the CSS classes .sort-caret and its modifiers .ascending/.descending in your stylesheet. The icons are rendered as <i> elements inside packages/table/src/table-header.js, allowing you to replace the default caret symbols with custom icons or SVG backgrounds through scoped CSS or deep selectors.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →