Skip to content

Rows sorting

Sort rows alphabetically or numerically, in ascending, descending, or a custom order, by one or multiple columns.

Overview

Handsontable provides two plugins for sorting rows:

  • ColumnSorting — sorts rows by a single column at a time. Clicking a column header cycles through ascending, descending, and unsorted states.
  • MultiColumnSorting — sorts rows by multiple columns simultaneously. Hold Ctrl/Cmd and click column headers to add more sort criteria.

Sorting runs when you release the mouse button, not when you press it, and only the header label and its sort indicator respond to the click. Pressing the header around them selects the column without sorting it. If the pointer moves while the button is held, Handsontable treats the gesture as a column drag instead of a sort, so column moving can share the same header.

Both plugins sort the view only. The source data array is never modified. To persist the sorted order back to the data source, see Saving data.

ColumnSorting and MultiColumnSorting are mutually exclusive. Enable only one at a time. If both options are set to true, ColumnSorting is automatically disabled.

Sorting demo

Click a column header to sort in ascending (↑) or descending (↓) order. Click again to return to the original order.

JavaScript
// to import sorting as an individual module, see the 'Import the sorting module' section of this page
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
// enable sorting for all columns
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
// to import sorting as an individual module, see the 'Import the sorting module' section of this page
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
// enable sorting for all columns
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Enable sorting

To enable sorting for all columns, set columnSorting to true.

<HotTable
columnSorting={true}
/>

To disable sorting for specific columns, set headerAction to false in the per-column configuration. In the following example, only the Model, Date, and In stock columns are sortable.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
// enable sorting for all columns
columnSorting={true}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
// disable sorting for the 'Brand' column
columnSorting: {
headerAction: false,
},
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
// disable sorting for the 'Price' column
columnSorting: {
headerAction: false,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
// disable sorting for the 'Time' column
columnSorting: {
headerAction: false,
},
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
// enable sorting for all columns
columnSorting={true}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
// disable sorting for the 'Brand' column
columnSorting: {
headerAction: false,
},
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
// disable sorting for the 'Price' column
columnSorting: {
headerAction: false,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
// disable sorting for the 'Time' column
columnSorting: {
headerAction: false,
},
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Configure sorting

Set columnSorting to an object to configure the plugin. See the Reference section below for all available options.

<HotTable
columnSorting={{
headerAction: true,
sortEmptyCells: false,
indicator: true,
sortFixedRows: false,
initialConfig: {
column: 1,
sortOrder: 'desc',
},
compareFunctionFactory(sortOrder, columnMeta) {
return function(value, nextValue) {
// return -1, 0, or 1
};
},
}}
/>

You can also override columnSorting options per column, using the columns configuration:

<HotTable
columnSorting={true}
columns={[
{
columnSorting: {
indicator: false,
headerAction: false,
},
},
]}
/>

Sort different types of data

Handsontable applies type-aware sorting automatically when you set the type option on a column. The supported cell types are:

You can also define a custom cell type. See Cell type.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ALLOWED_TAGS = ['BR', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH'];
const ALLOWED_ATTRIBUTES = ['colspan', 'rowspan'];
const DROPPED_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE'];
// Handsontable has no built-in sanitizer since v18.0, and `sanitizer` is grid-level:
// it also filters pasted HTML, so the table tags have to survive -- otherwise pasting
// a range degrades to plain text. In production, use a vetted library such as DOMPurify.
// See https://handsontable.com/docs/security/
const sanitizeHeader = (html) => {
const template = document.createElement('template');
template.innerHTML = html;
template.content.querySelectorAll('*').forEach((element) => {
if (DROPPED_TAGS.includes(element.tagName)) {
// Unwrapping these would promote their source text into the output
element.remove();
} else if (ALLOWED_TAGS.includes(element.tagName)) {
Array.from(element.attributes).forEach((attribute) => {
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
element.removeAttribute(attribute.name);
}
});
} else {
// Unwrap a disallowed element, keeping its text content
element.replaceWith(...Array.from(element.childNodes));
}
});
return template.innerHTML;
};
const ExampleComponent = () => {
return (
<HotTable
data={[
{
model: 'Racing Socks',
size: 'S',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
color: 'Black',
email: '8576@all.xyz',
},
{
model: 'HL Mountain Shirt',
size: 'XS',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
color: 'White',
email: 'tayn@all.xyz',
},
{
model: 'Cycling Cap',
size: 'L',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
color: 'Green',
email: '6lights@far.com',
},
{
model: 'Ski Jacket',
size: 'M',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
color: 'Blue',
email: 'raj@fq1my2c.com',
},
{
model: 'HL Goggles',
size: 'XL',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
color: 'Black',
email: 'da@pdc.ga',
},
]}
columns={[
{
title: 'Model<br>(text)',
// set the type of the 'Model' column
type: 'text',
data: 'model',
},
{
title: 'Price<br>(numeric)',
// set the type of the 'Price' column
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Sold on<br>(date)',
// set the type of the 'Date' column
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time<br>(time)',
// set the type of the 'Time' column
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock<br>(checkbox)',
// set the type of the 'In stock' column
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
{
title: 'Size<br>(dropdown)',
// set the type of the 'Size' column
type: 'dropdown',
data: 'size',
source: ['XS', 'S', 'M', 'L', 'XL'],
className: 'htCenter',
},
{
title: 'Color<br>(autocomplete)',
// set the type of the 'Size' column
type: 'autocomplete',
data: 'color',
source: ['White', 'Black', 'Yellow', 'Blue', 'Green'],
className: 'htCenter',
},
{
title: 'Email<br>(password)',
// set the type of the 'Email' column
type: 'password',
data: 'email',
},
]}
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
sanitizer={sanitizeHeader}
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ALLOWED_TAGS = ['BR', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH'];
const ALLOWED_ATTRIBUTES = ['colspan', 'rowspan'];
const DROPPED_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE'];
// Handsontable has no built-in sanitizer since v18.0, and `sanitizer` is grid-level:
// it also filters pasted HTML, so the table tags have to survive -- otherwise pasting
// a range degrades to plain text. In production, use a vetted library such as DOMPurify.
// See https://handsontable.com/docs/security/
const sanitizeHeader = (html: string): string => {
const template = document.createElement('template');
template.innerHTML = html;
template.content.querySelectorAll('*').forEach((element) => {
if (DROPPED_TAGS.includes(element.tagName)) {
// Unwrapping these would promote their source text into the output
element.remove();
} else if (ALLOWED_TAGS.includes(element.tagName)) {
Array.from(element.attributes).forEach((attribute) => {
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
element.removeAttribute(attribute.name);
}
});
} else {
// Unwrap a disallowed element, keeping its text content
element.replaceWith(...Array.from(element.childNodes));
}
});
return template.innerHTML;
};
const ExampleComponent = () => {
return (
<HotTable
data={[
{
model: 'Racing Socks',
size: 'S',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
color: 'Black',
email: '8576@all.xyz',
},
{
model: 'HL Mountain Shirt',
size: 'XS',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
color: 'White',
email: 'tayn@all.xyz',
},
{
model: 'Cycling Cap',
size: 'L',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
color: 'Green',
email: '6lights@far.com',
},
{
model: 'Ski Jacket',
size: 'M',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
color: 'Blue',
email: 'raj@fq1my2c.com',
},
{
model: 'HL Goggles',
size: 'XL',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
color: 'Black',
email: 'da@pdc.ga',
},
]}
columns={[
{
title: 'Model<br>(text)',
// set the type of the 'Model' column
type: 'text', // 'text' is the default type, so you can omit this line
data: 'model',
},
{
title: 'Price<br>(numeric)',
// set the type of the 'Price' column
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Sold on<br>(date)',
// set the type of the 'Date' column
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time<br>(time)',
// set the type of the 'Time' column
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock<br>(checkbox)',
// set the type of the 'In stock' column
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
{
title: 'Size<br>(dropdown)',
// set the type of the 'Size' column
type: 'dropdown',
data: 'size',
source: ['XS', 'S', 'M', 'L', 'XL'],
className: 'htCenter',
},
{
title: 'Color<br>(autocomplete)',
// set the type of the 'Size' column
type: 'autocomplete',
data: 'color',
source: ['White', 'Black', 'Yellow', 'Blue', 'Green'],
className: 'htCenter',
},
{
title: 'Email<br>(password)',
// set the type of the 'Email' column
type: 'password',
data: 'email',
},
]}
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
sanitizer={sanitizeHeader}
/>
);
};
export default ExampleComponent;

Set an initial sort order

Use the initialConfig option to apply a sort order when Handsontable initializes. column is the visual column index. sortOrder is 'asc' for ascending or 'desc' for descending.

<HotTable
columnSorting={{
initialConfig: {
column: 0,
sortOrder: 'asc',
},
}}
/>

To set an initial sort order across multiple columns, use the MultiColumnSorting plugin with an array value for initialConfig. See Set an initial multi-column sort order.

Add a custom comparator

A comparator is a function that determines sort order based on two cell values. Use a custom comparator to implement sorting logic beyond Handsontable’s built-in defaults.

Common use cases:

  • Sort by value length, occurrence of a character, or any other custom criterion.
  • Exclude specific rows from sorting (for example, rows with a particular job title).

Use the compareFunctionFactory option to provide a comparator factory. The factory receives sortOrder ('asc' or 'desc') and columnMeta, and must return a comparator function. The comparator receives two cell values and must return -1, 0, or 1.

<HotTable
columnSorting={{
compareFunctionFactory: function(sortOrder, columnMeta) {
return function(value, nextValue) {
if (value < nextValue) return sortOrder === 'asc' ? -1 : 1;
if (value > nextValue) return sortOrder === 'asc' ? 1 : -1;
return 0;
};
},
}}
/>

Use sorting hooks

Run code before or after sorting using the following Handsontable hooks:

  • beforeColumnSort — fires before sorting. Return false to cancel the sort and keep the current order.
  • afterColumnSort — fires after sorting completes.

A common use of beforeColumnSort is server-side sorting: cancel the client-side sort, send the sort configuration to a server, and reload the data. The following example simulates this: it cancels the front-end sort, “asks a server” to sort the rows, and loads the sorted rows back into the grid.

A common use of afterColumnSort is excluding specific rows from the sorted result — see the afterColumnSort example in the next section. Frozen rows need no hook at all: they stay out of the sort by default.

JavaScript
import { useRef, useState } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const data = [
{ brand: 'Jetpulse', model: 'Racing Socks', price: 30, sellDate: '2023-10-11' },
{ brand: 'Gigabox', model: 'HL Mountain Frame', price: 1890.9, sellDate: '2023-05-03' },
{ brand: 'Camido', model: 'Cycling Cap', price: 130.1, sellDate: '2023-03-27' },
{ brand: 'Chatterpoint', model: 'Road Tire Tube', price: 59, sellDate: '2023-08-28' },
{ brand: 'Eidel', model: 'HL Road Tire', price: 279.99, sellDate: '2023-10-02' },
];
const columnDataKeys = ['brand', 'model', 'price', 'sellDate'];
// Simulates a server that receives a sort request and returns sorted rows.
function sortOnServer(columnKey, sortOrder) {
return new Promise((resolve) => {
setTimeout(() => {
const sortedData = [...data].sort((rowA, rowB) => {
if (rowA[columnKey] === rowB[columnKey]) {
return 0;
}
return (rowA[columnKey] > rowB[columnKey]) === (sortOrder === 'asc') ? 1 : -1;
});
resolve(sortedData);
}, 600);
});
}
const ExampleComponent = () => {
// `data` is kept in state (instead of loaded imperatively) so a `status` update doesn't
// make HotTable re-apply the original `data` prop and undo the sort.
const [gridData, setGridData] = useState(data);
const [status, setStatus] = useState('Click a column header to sort.');
// Canceling the front-end sort also stops Handsontable from tracking the column's sort
// order, so this example cycles ascending -> descending -> unsorted manually.
const activeSortRef = useRef(null);
const getNextSortOrder = (column) => {
const activeSort = activeSortRef.current;
if (!activeSort || activeSort.column !== column) {
return 'asc';
}
return activeSort.sortOrder === 'asc' ? 'desc' : null;
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<span>{status}</span>
</div>
</div>
<HotTable
data={gridData}
columns={[
{ title: 'Brand', type: 'text', data: 'brand' },
{ title: 'Model', type: 'text', data: 'model' },
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
]}
columnSorting={true}
beforeColumnSort={(currentSortConfig, destinationSortConfigs) => {
const [requestedSort] = destinationSortConfigs;
if (!requestedSort) {
// the sorting was cleared programmatically, restore the original row order
activeSortRef.current = null;
setGridData(data);
return false;
}
const nextOrder = getNextSortOrder(requestedSort.column);
if (nextOrder === null) {
activeSortRef.current = null;
setStatus('Cleared the sort.');
setGridData(data);
return false;
}
activeSortRef.current = { column: requestedSort.column, sortOrder: nextOrder };
setStatus('Sorting on the server...');
sortOnServer(columnDataKeys[requestedSort.column], nextOrder).then((sortedData) => {
setGridData(sortedData);
setStatus('Sorted on the server.');
});
// return `false` to cancel Handsontable's own front-end sort
return false;
}}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef, useState } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const data = [
{ brand: 'Jetpulse', model: 'Racing Socks', price: 30, sellDate: '2023-10-11' },
{ brand: 'Gigabox', model: 'HL Mountain Frame', price: 1890.9, sellDate: '2023-05-03' },
{ brand: 'Camido', model: 'Cycling Cap', price: 130.1, sellDate: '2023-03-27' },
{ brand: 'Chatterpoint', model: 'Road Tire Tube', price: 59, sellDate: '2023-08-28' },
{ brand: 'Eidel', model: 'HL Road Tire', price: 279.99, sellDate: '2023-10-02' },
];
const columnDataKeys = ['brand', 'model', 'price', 'sellDate'];
// Simulates a server that receives a sort request and returns sorted rows.
function sortOnServer(columnKey: string, sortOrder: string) {
return new Promise<typeof data>((resolve) => {
setTimeout(() => {
const sortedData = [...data].sort((rowA: any, rowB: any) => {
if (rowA[columnKey] === rowB[columnKey]) {
return 0;
}
return (rowA[columnKey] > rowB[columnKey]) === (sortOrder === 'asc') ? 1 : -1;
});
resolve(sortedData);
}, 600);
});
}
const ExampleComponent = () => {
// `data` is kept in state (instead of loaded imperatively) so a `status` update doesn't
// make HotTable re-apply the original `data` prop and undo the sort.
const [gridData, setGridData] = useState(data);
const [status, setStatus] = useState('Click a column header to sort.');
// Canceling the front-end sort also stops Handsontable from tracking the column's sort
// order, so this example cycles ascending -> descending -> unsorted manually.
const activeSortRef = useRef<{ column: number; sortOrder: 'asc' | 'desc' } | null>(null);
const getNextSortOrder = (column: number): 'asc' | 'desc' | null => {
const activeSort = activeSortRef.current;
if (!activeSort || activeSort.column !== column) {
return 'asc';
}
return activeSort.sortOrder === 'asc' ? 'desc' : null;
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<span>{status}</span>
</div>
</div>
<HotTable
data={gridData}
columns={[
{ title: 'Brand', type: 'text', data: 'brand' },
{ title: 'Model', type: 'text', data: 'model' },
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
]}
columnSorting={true}
beforeColumnSort={(currentSortConfig, destinationSortConfigs) => {
const [requestedSort] = destinationSortConfigs;
if (!requestedSort) {
// the sorting was cleared programmatically, restore the original row order
activeSortRef.current = null;
setGridData(data);
return false;
}
const nextOrder = getNextSortOrder(requestedSort.column);
if (nextOrder === null) {
activeSortRef.current = null;
setStatus('Cleared the sort.');
setGridData(data);
return false;
}
activeSortRef.current = { column: requestedSort.column, sortOrder: nextOrder };
setStatus('Sorting on the server...');
sortOnServer(columnDataKeys[requestedSort.column], nextOrder).then((sortedData) => {
setGridData(sortedData);
setStatus('Sorted on the server.');
});
// return `false` to cancel Handsontable's own front-end sort
return false;
}}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;

Exclude rows from sorting

Two cases are worth telling apart. Rows you froze with fixedRowsTop or fixedRowsBottom are left out of sorting for you, with no code at all. Rows that are not frozen take part in every sort, and keeping one of them in place is something you write yourself.

Frozen rows stay out of the sort by default

Since version 18.0.0, a frozen row holds its position no matter which column you sort by. This matters when a frozen row at the top carries column labels, or a frozen row at the bottom carries column summaries whose formulas point at absolute cell addresses. Sorting such a row into the middle of the data would break those formulas.

Both sorting plugins behave this way: ColumnSorting and MultiColumnSorting.

Formulas from the Formulas plugin (HyperFormula) in the sortable body are a different case. Sorting a column of those formula cells can produce #REF!. Further sorts do not recover the original references. Handsontable does not rewrite formula references to follow sorted rows.

Keep aggregate HyperFormula formulas on a fixedRowsBottom row so they stay out of the sortable body. That is the supported workaround (#12627). A hook that pins a non-frozen totals row does not protect other formula cells in the body. See Sorting formula cells.

To sort the whole dataset instead, frozen rows included, set the sortFixedRows option to true. That restores the behavior Handsontable had before version 18.0.0.

In the example below, both grids freeze a Target row at the top and a Total row at the bottom, and both start sorted by revenue, highest first. In the first grid, the frozen rows keep their place. In the second, sortFixedRows is true, so they are sorted with the rest of the data: Total moves to the top, and Target lands between two regions. Click any column header to compare the two grids.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
// The first row holds a target and the last one a total. Both are frozen, so they stay in
// view while you scroll.
const getData = () => [
['Target', 30000, 225],
['North', 42300, 318],
['South', 18750, 142],
['East', 27900, 205],
['West', 35100, 264],
['Total', 124050, 929],
];
// Everything the two grids share. Only `sortFixedRows` differs between them.
const sharedSettings = {
colHeaders: ['Region', 'Revenue', 'Orders'],
columns: [
{ type: 'text' },
{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0,
},
},
{ type: 'numeric' },
],
fixedRowsTop: 1,
fixedRowsBottom: 1,
height: 'auto',
stretchH: 'all',
licenseKey: 'non-commercial-and-evaluation',
};
// Both grids start sorted by revenue, highest first.
const initialConfig = { column: 1, sortOrder: 'desc' };
// `sortFixedRows: false` is the default: the frozen rows keep their place whatever you sort by.
const defaultSorting = { initialConfig };
const allRowsSorting = { sortFixedRows: true, initialConfig };
const defaultData = getData();
const allRowsData = getData();
const ExampleComponent = () => (<>
<h3 className="demo-preview">Default: the frozen rows keep their place</h3>
<HotTable {...sharedSettings} data={defaultData} columnSorting={defaultSorting}/>
<h3 className="demo-preview">
With <code>sortFixedRows: true</code>: the frozen rows are sorted too
</h3>
<HotTable {...sharedSettings} data={allRowsData} columnSorting={allRowsSorting}/>
</>);
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
// register Handsontable's modules
registerAllModules();
// The first row holds a target and the last one a total. Both are frozen, so they stay in
// view while you scroll.
const getData = () => [
['Target', 30000, 225],
['North', 42300, 318],
['South', 18750, 142],
['East', 27900, 205],
['West', 35100, 264],
['Total', 124050, 929],
];
// Everything the two grids share. Only `sortFixedRows` differs between them.
const sharedSettings: GridSettings = {
colHeaders: ['Region', 'Revenue', 'Orders'],
columns: [
{ type: 'text' },
{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0,
},
},
{ type: 'numeric' },
],
fixedRowsTop: 1,
fixedRowsBottom: 1,
height: 'auto',
stretchH: 'all',
licenseKey: 'non-commercial-and-evaluation',
};
// Both grids start sorted by revenue, highest first.
const initialConfig = { column: 1, sortOrder: 'desc' as const };
// `sortFixedRows: false` is the default: the frozen rows keep their place whatever you sort by.
const defaultSorting = { initialConfig };
const allRowsSorting = { sortFixedRows: true, initialConfig };
const defaultData = getData();
const allRowsData = getData();
const ExampleComponent = () => (
<>
<h3 className="demo-preview">Default: the frozen rows keep their place</h3>
<HotTable {...sharedSettings} data={defaultData} columnSorting={defaultSorting} />
<h3 className="demo-preview">
With <code>sortFixedRows: true</code>: the frozen rows are sorted too
</h3>
<HotTable {...sharedSettings} data={allRowsData} columnSorting={allRowsSorting} />
</>
);
export default ExampleComponent;

sortFixedRows is a grid-level option, so you cannot set it per column: the frozen rows belong to the whole table rather than to one column. If you set it inside columns, it has no effect, and Handsontable logs a warning to the console.

Exclude rows that are not frozen

A row that is not frozen takes part in every sort. To hold one in place, listen to the afterColumnSort hook and move the row back with rowIndexMapper.moveIndexes().

In the example below, the two featured products stay at the top whichever column you sort by. No row is frozen. The rule reads the data rather than a row position, so you pin a row by flagging it instead of by knowing where it sits.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
// The two featured products always stay at the top, whichever column you sort by.
// No row here is frozen: rows frozen with `fixedRowsTop` or `fixedRowsBottom` are already
// left out of sorting, and need no code at all.
const products = [
{ name: 'HL Mountain Frame', price: 1890.9, inStock: 11, featured: true },
{ name: 'HL Road Tire', price: 279.99, inStock: 3, featured: true },
{ name: 'Cycling Cap', price: 130.1, inStock: 0 },
{ name: 'Road Tire Tube', price: 59, inStock: 1 },
{ name: 'Racing Socks', price: 30, inStock: 5 },
{ name: 'Water Bottle', price: 12.5, inStock: 24 },
{ name: 'Bike Light', price: 45, inStock: 8 },
{ name: 'Chain Lube', price: 9.99, inStock: 17 },
];
// Move the featured rows back to the top of the view. The rule reads the data, not a row
// position, so you pin a row by flagging it rather than by knowing where it sits.
// Handsontable calls a hook with the grid as `this`, which is set even for a sort that runs
// while the grid is being created - a component ref is not filled in yet at that point.
function pinFeaturedRows() {
const featuredRows = products
.map((product, physicalRow) => (product.featured ? this.toVisualRow(physicalRow) : null))
.filter((visualRow) => visualRow !== null);
// Pass every index in one call: each move shifts the rows after it.
this.rowIndexMapper.moveIndexes(featuredRows, 0);
}
const ExampleComponent = () => {
return (
<HotTable
data={products}
columns={[
{ data: 'name', type: 'text' },
{
data: 'price',
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ data: 'inStock', type: 'numeric' },
]}
colHeaders={['Product', 'Price', 'In stock']}
height="auto"
stretchH="all"
columnSorting={true}
// `afterColumnSort()` is a Handsontable hook: it's fired after each sorting
afterColumnSort={pinFeaturedRows}
// `cells()` receives a physical row index, so it reads the source array directly.
cells={(row) => (products[row]?.featured ? { className: 'featured-product' } : {})}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import type Handsontable from 'handsontable/base';
// register Handsontable's modules
registerAllModules();
type Product = {
name: string;
price: number;
inStock: number;
featured?: boolean;
};
// The two featured products always stay at the top, whichever column you sort by.
// No row here is frozen: rows frozen with `fixedRowsTop` or `fixedRowsBottom` are already
// left out of sorting, and need no code at all.
const products: Product[] = [
{ name: 'HL Mountain Frame', price: 1890.9, inStock: 11, featured: true },
{ name: 'HL Road Tire', price: 279.99, inStock: 3, featured: true },
{ name: 'Cycling Cap', price: 130.1, inStock: 0 },
{ name: 'Road Tire Tube', price: 59, inStock: 1 },
{ name: 'Racing Socks', price: 30, inStock: 5 },
{ name: 'Water Bottle', price: 12.5, inStock: 24 },
{ name: 'Bike Light', price: 45, inStock: 8 },
{ name: 'Chain Lube', price: 9.99, inStock: 17 },
];
// Move the featured rows back to the top of the view. The rule reads the data, not a row
// position, so you pin a row by flagging it rather than by knowing where it sits.
// Handsontable calls a hook with the grid as `this`, which is set even for a sort that runs
// while the grid is being created - a component ref is not filled in yet at that point.
function pinFeaturedRows(this: Handsontable) {
const featuredRows = products
.map((product, physicalRow) => (product.featured ? this.toVisualRow(physicalRow) : null))
.filter((visualRow): visualRow is number => visualRow !== null);
// Pass every index in one call: each move shifts the rows after it.
this.rowIndexMapper.moveIndexes(featuredRows, 0);
}
const ExampleComponent = () => {
return (
<HotTable
data={products}
columns={[
{ data: 'name', type: 'text' },
{
data: 'price',
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ data: 'inStock', type: 'numeric' },
]}
colHeaders={['Product', 'Price', 'In stock']}
height="auto"
stretchH="all"
columnSorting={true}
// `afterColumnSort()` is a Handsontable hook: it's fired after each sorting
afterColumnSort={pinFeaturedRows}
// `cells()` receives a physical row index, so it reads the source array directly.
cells={(row: number) => (products[row]?.featured ? { className: 'featured-product' } : {})}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
CSS
/* Marks the featured rows, so you can watch them hold their place while the rest of the list
sorts. The background is set through the theme's own CSS variables rather than with a plain
`background-color`: the theme's row-striping rule is more specific than a single class, so a
direct declaration would be ignored. */
.handsontable td.featured-product {
--ht-row-cell-odd-background-color: #fff8e1;
--ht-row-cell-even-background-color: #fff8e1;
font-weight: 600;
}

Control sorting programmatically

Use the ColumnSorting plugin API and updateSettings() to control sorting at runtime. This lets you, for example, enable or disable sorting based on conditions, or trigger sorting from outside the grid.

To access Handsontable’s API methods from a React component, see Instance methods.

Enable or disable sorting programmatically

To enable or disable sorting programmatically, call updateSettings() with columnSorting set to true or false.

const hotTableComponentRef = useRef(null);
// enable sorting for all columns
hotTableComponentRef.current.hotInstance.updateSettings({
columnSorting: true,
});
// disable sorting for all columns
hotTableComponentRef.current.hotInstance.updateSettings({
columnSorting: false,
});
// enable sorting on column 0, disable sorting on column 1
hotTableComponentRef.current.hotInstance.updateSettings({
columns: [
{ columnSorting: { headerAction: true } },
{ columnSorting: { headerAction: false } },
],
});

Sort data programmatically

Use columnSorting.sort() to sort programmatically. Pass an object with column (visual column index) and sortOrder ('asc' or 'desc'). Each call replaces the previous sort order entirely.

Use columnSorting.clearSort() to remove the active sort and return rows to their original order.

const hotTableComponentRef = useRef(null);
const columnSorting = hotTableComponentRef.current.hotInstance.getPlugin('columnSorting');
// sort column 0 in ascending order
columnSorting.sort({ column: 0, sortOrder: 'asc' });
// return rows to their original order
columnSorting.clearSort();

To see how it works, try out the following demo:

JavaScript
import { useRef } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotTableComponentRef = useRef(null);
const sortAsc = () => {
// get the `ColumnSorting` plugin
const columnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('columnSorting');
columnSorting?.sort({
column: 0,
sortOrder: 'asc',
});
};
const unsort = () => {
// get the `ColumnSorting` plugin
const columnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('columnSorting');
columnSorting?.clearSort();
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={sortAsc}>Sort by the "Brand" column, in ascending order</button>
<button onClick={unsort}>Go back to the original order</button>
</div>
</div>
<HotTable
ref={hotTableComponentRef}
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotTableComponentRef = useRef<HotTableRef>(null);
const sortAsc = () => {
// get the `ColumnSorting` plugin
const columnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('columnSorting');
columnSorting?.sort({
column: 0,
sortOrder: 'asc',
});
};
const unsort = () => {
// get the `ColumnSorting` plugin
const columnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('columnSorting');
columnSorting?.clearSort();
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={sortAsc}>Sort by the "Brand" column, in ascending order</button>
<button onClick={unsort}>Go back to the original order</button>
</div>
</div>
<HotTable
ref={hotTableComponentRef}
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
columnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;

MultiColumnSorting plugin

The MultiColumnSorting plugin extends ColumnSorting to sort rows by multiple columns at the same time.

Key differences from ColumnSorting:

  • Sort by multiple columns at once. The column clicked first has the highest sort priority.
  • Hold Ctrl/ and click a column header to add it to the active sort criteria without replacing the existing sort.
  • Press Shift+Enter with a column header focused to append that column to the active sort criteria.
  • initialConfig accepts an array of sort config objects to define a multi-column initial order.

The Shift+Enter shortcut requires a focused column header. Enable navigableHeaders: true to move focus onto headers with the arrow keys.

ColumnSorting and MultiColumnSorting are mutually exclusive. If both are set to true, ColumnSorting is automatically disabled.

Enable multi-column sorting

To enable multi-column sorting for all columns, set multiColumnSorting to true.

<HotTable
multiColumnSorting={true}
/>

Configure multi-column sorting options

multiColumnSorting supports the same options as columnSorting: headerAction, sortEmptyCells, indicator, sortFixedRows, and compareFunctionFactory. Refer to Configure sorting for a description of each option.

To disable multi-column sorting for a specific column, set headerAction to false in that column’s configuration:

<HotTable
multiColumnSorting={true}
columns={[
{
multiColumnSorting: {
headerAction: false,
},
},
]}
/>

Sort by multiple columns

To sort by multiple columns interactively, hold Ctrl/ and click column headers in the desired priority order.

Try the following demo:

  1. Click Brand. The rows sort by brand.
  2. Hold Ctrl/ and click Model. The rows sort by model within each brand.
  3. Hold Ctrl/ and click Price. The rows sort by price within each model.
JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 30,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 279.99,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 59,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
// enable sorting by multiple columns, for all columns
multiColumnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 30,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 279.99,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 59,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
// enable sorting by multiple columns, for all columns
multiColumnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Set an initial multi-column sort order

Use initialConfig with an array of sort config objects to apply a multi-column sort at initialization. Each object has a column property (visual column index) and a sortOrder property ('asc' or 'desc'). The array order determines sort priority: the first entry has the highest priority.

In the following demo, the data is initially sorted by Brand ascending, then by Model descending:

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={{
initialConfig: [
{
// at initialization, sort the data by the 'Brand' column, in ascending order
column: 0,
sortOrder: 'asc',
},
// at initialization, sort the data by the 'Model' column, in descending order
{
column: 1,
sortOrder: 'desc',
},
],
}}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={{
initialConfig: [
{
// at initialization, sort the data by the 'Brand' column, in ascending order
column: 0,
sortOrder: 'asc',
},
// at initialization, sort the data by the 'Model' column, in descending order
{
column: 1,
sortOrder: 'desc',
},
],
}}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
<HotTable
multiColumnSorting={{
initialConfig: [
{ column: 0, sortOrder: 'asc' },
{ column: 1, sortOrder: 'desc' },
],
}}
/>

Sort by multiple columns programmatically

Use multiColumnSorting.sort() to sort by multiple columns programmatically. Pass an array of sort config objects. The array order determines sort priority. Each call replaces the previous sort order entirely.

Use multiColumnSorting.clearSort() to remove all sort criteria and return rows to their original order.

const hotTableComponentRef = useRef(null);
const multiColumnSorting = hotTableComponentRef.current.hotInstance.getPlugin('multiColumnSorting');
// sort column 0 ascending, then column 1 descending
multiColumnSorting.sort([
{ column: 0, sortOrder: 'asc' },
{ column: 1, sortOrder: 'desc' },
]);
// return rows to their original order
multiColumnSorting.clearSort();

To see how it works, try out the following demo:

JavaScript
import { useRef } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotTableComponentRef = useRef(null);
const sort = () => {
// get the `MultiColumnSorting` plugin
const multiColumnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('multiColumnSorting');
multiColumnSorting?.sort([
{
column: 0,
sortOrder: 'asc',
},
{
column: 1,
sortOrder: 'desc',
},
]);
};
return (
<>
<HotTable
ref={hotTableComponentRef}
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
<div className="example-controls-container">
<div className="controls">
<button onClick={sort}>Sort</button>
</div>
</div>
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotTableComponentRef = useRef<HotTableRef>(null);
const sort = () => {
// get the `MultiColumnSorting` plugin
const multiColumnSorting = hotTableComponentRef.current?.hotInstance?.getPlugin('multiColumnSorting');
multiColumnSorting?.sort([
{
column: 0,
sortOrder: 'asc',
},
{
column: 1,
sortOrder: 'desc',
},
]);
};
return (
<>
<HotTable
ref={hotTableComponentRef}
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Jetpulse',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={true}
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
<div className="example-controls-container">
<div className="controls">
<button onClick={sort}>Sort</button>
</div>
</div>
</>
);
};
export default ExampleComponent;

Add custom sort icons

The default sort icons (↑↓) are rendered using CSS -webkit-mask-image. Override the following pseudo-elements to replace them:

  • .columnSorting.sortAction.ascending::before
  • .columnSorting.sortAction.descending::before
JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
columnSorting={{
initialConfig: {
column: 1,
sortOrder: 'desc',
},
}}
className="custom-sort-icon-example-1"
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Mountain Frame',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
columnSorting={{
initialConfig: {
column: 1,
sortOrder: 'desc',
},
}}
className="custom-sort-icon-example-1"
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
CSS
.custom-sort-icon-example-1.handsontable .columnSorting.sortAction.ascending::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M0 0h24v24H0z' stroke='none'/%3E%3Cpath d='M12 21V9M8 13l4-4 4 4'/%3E%3Cpath d='M21 12a9 9 0 0 0-18 0'/%3E%3C/svg%3E");
}
.custom-sort-icon-example-1.handsontable .columnSorting.sortAction.descending::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M0 0h24v24H0z' stroke='none'/%3E%3Cpath d='M12 3v12M16 11l-4 4-4-4'/%3E%3Cpath d='M3 12a9 9 0 0 0 18 0'/%3E%3C/svg%3E");
}

To replace the column-priority number indicators used by the MultiColumnSorting plugin (1, 2, etc.), override the content of .columnSorting.sort-1::after and subsequent pseudo-elements:

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
color: 'White',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Frame',
color: 'Black',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
color: 'Red',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
color: 'Green',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
color: 'Blue',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Color',
type: 'text',
data: 'color',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={{
initialConfig: [
{
column: 0,
sortOrder: 'asc',
},
{
column: 1,
sortOrder: 'desc',
},
{
column: 2,
sortOrder: 'asc',
},
{
column: 3,
sortOrder: 'desc',
},
{
column: 4,
sortOrder: 'asc',
},
{
column: 5,
sortOrder: 'desc',
},
{
column: 6,
sortOrder: 'asc',
},
],
}}
className="custom-sort-icon-example-3"
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
{
brand: 'Jetpulse',
model: 'Racing Socks',
color: 'White',
price: 30,
sellDate: '2023-10-11',
sellTime: '01:23',
inStock: false,
},
{
brand: 'Gigabox',
model: 'HL Frame',
color: 'Black',
price: 1890.9,
sellDate: '2023-05-03',
sellTime: '11:27',
inStock: false,
},
{
brand: 'Camido',
model: 'Cycling Cap',
color: 'Red',
price: 130.1,
sellDate: '2023-03-27',
sellTime: '03:17',
inStock: true,
},
{
brand: 'Chatterpoint',
model: 'Road Tire Tube',
color: 'Green',
price: 59,
sellDate: '2023-08-28',
sellTime: '08:01',
inStock: true,
},
{
brand: 'Eidel',
model: 'HL Road Tire',
color: 'Blue',
price: 279.99,
sellDate: '2023-10-02',
sellTime: '13:23',
inStock: true,
},
]}
columns={[
{
title: 'Brand',
type: 'text',
data: 'brand',
},
{
title: 'Model',
type: 'text',
data: 'model',
},
{
title: 'Color',
type: 'text',
data: 'color',
},
{
title: 'Price',
type: 'numeric',
data: 'price',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
title: 'Date',
type: 'intl-date',
data: 'sellDate',
locale: 'en-US',
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
className: 'htRight',
},
{
title: 'Time',
type: 'intl-time',
data: 'sellTime',
locale: 'en-US',
timeFormat: { hour: '2-digit', minute: '2-digit', hour12: true },
className: 'htRight',
},
{
title: 'In stock',
type: 'checkbox',
data: 'inStock',
className: 'htCenter',
},
]}
multiColumnSorting={{
initialConfig: [
{
column: 0,
sortOrder: 'asc',
},
{
column: 1,
sortOrder: 'desc',
},
{
column: 2,
sortOrder: 'asc',
},
{
column: 3,
sortOrder: 'desc',
},
{
column: 4,
sortOrder: 'asc',
},
{
column: 5,
sortOrder: 'desc',
},
{
column: 6,
sortOrder: 'asc',
},
],
}}
className="custom-sort-icon-example-3"
height="auto"
stretchH="all"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
CSS
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-1::after {
content: '①';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-2::after {
content: '②';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-3::after {
content: '③';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-4::after {
content: '④';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-5::after {
content: '⑤';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-6::after {
content: '⑥';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting.sort-7::after {
content: '⑦';
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable span.colHeader.columnSorting::after {
width: 10px;
font-size: 10px;
}
#exampleCustomSortIcons3 .custom-sort-icon-example-3 .handsontable .columnSorting.sortAction::before {
right: 5px;
}

Import the sorting module

To reduce bundle size, import only the modules you need. For sorting, you need the base module and the plugin module.

// import the base module
import Handsontable from 'handsontable/base';
// import ColumnSorting (or MultiColumnSorting instead)
import { registerPlugin, ColumnSorting } from 'handsontable/plugins';
// register the plugin
registerPlugin(ColumnSorting);

Result

After completing this guide, users can sort rows by clicking column headers, and you can control sort order programmatically. You can use ColumnSorting for single-column sorting or MultiColumnSorting for multi-column sorting with custom priority.

These header-focused shortcuts work only when a column header is focused. Enable navigableHeaders: true to move focus onto headers with the arrow keys. For more details, see Keyboard navigation.

WindowsmacOSActionExcelSheets
EnterEnterSort by the focused column, cycling through ascending, descending, and original order
Shift+Enter+EnterAppend the focused column to the active sort criteria. Requires the MultiColumnSorting plugin.

Known limitations

  • Sorting a column of Formulas (HyperFormula) cells can produce #REF!. Handsontable does not rewrite those addresses to follow sorted rows, and a later sort does not restore the formulas. Keep summary formulas on a fixedRowsBottom row, and leave sortFixedRows at false so the footer stays out of the sort. See Sorting formula cells.

Reference

Plugin comparison

FeatureColumnSortingMultiColumnSorting
Sort by single columnYesYes
Sort by multiple columnsNoYes
Ctrl/Cmd + click to add columnsNoYes
initialConfig typeObjectArray of objects
Mutual exclusivityDisabled when MultiColumnSorting is enabledDisables ColumnSorting

Configuration options

OptionTypeDefaultDescription
headerActionbooleantrueWhen true, clicking a column header sorts by that column.
sortEmptyCellsbooleanfalseWhen true, empty cells participate in sorting. When false, empty cells are always placed at the end.
sortFixedRowsbooleanfalseWhen true, the rows frozen by fixedRowsTop and fixedRowsBottom are sorted along with the rest of the dataset. When false, they keep their position. Grid-level only.
indicatorbooleantrueWhen true, a sort-order arrow icon is shown in the column header.
compareFunctionFactoryfunctionA factory that returns a custom comparator function. See Add a custom comparator.
initialConfigobjectSort config applied at initialization. Contains column (visual index) and sortOrder ('asc' or 'desc').

API reference

For the full list of options, methods, and hooks related to sorting, see the following API reference pages:

Troubleshooting

Didn’t find what you need? Try this: