Excel-Style Filter
Windows Excel mode turns every selection-filter column into a staged checklist modeled after the Windows Excel AutoFilter menu. Users search or check values, review the draft, and then choose Apply or Cancel.
Source code
grid.plugins = [AdvanceFilterPlugin];
grid.columns = [
{ prop: 'status', name: 'Status', filter: [FIlTER_SELECTION] },
];
grid.filter = {
selection: {
excelMode: 'windows',
},
};
return [
{ status: 'Open' },
{ status: 'Closed' },
{ status: 'Pending' },
{ status: 'Closed' },
];Quick start
Section titled “Quick start”Register AdvanceFilterPlugin, enable the selection family on a column, and set the global selection preset to windows:
import { AdvanceFilterPlugin, FIlTER_SELECTION } from '@revolist/revogrid-pro';import '@revolist/revogrid-pro/dist/revogrid-pro.css';
grid.plugins = [AdvanceFilterPlugin];grid.columns = [ { name: 'Status', prop: 'status', filter: [FIlTER_SELECTION] }, { name: 'Owner', prop: 'owner', filter: ['string'] },];grid.filter = { selection: { excelMode: 'windows', sortDirection: 'asc', },};grid.source = [ { status: 'Open', owner: 'Ana' }, { status: 'Closed', owner: 'Maks' },];selection.excelMode is grid-wide selection configuration, but it only changes columns that include the selection filter. The Owner column above keeps its ordinary text-filter popup.
Source code
// src/components/filter-selection/FilterAdvanced.ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { currentTheme } from '../composables/useRandomData';
import {
createFilterSelectionColumns,
createFilterSelectionFilter,
createFilterSelectionRows,
filterSelectionColumnTypes,
filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';
defineCustomElements();
const { isDark } = currentTheme();
export function load(parentSelector: string) {
const grid = document.createElement('revo-grid');
grid.columns = createFilterSelectionColumns();
grid.columnTypes = filterSelectionColumnTypes;
grid.plugins = filterSelectionPlugins;
grid.stretch = 'last';
grid.filter = createFilterSelectionFilter();
grid.theme = isDark() ? 'darkCompact' : 'compact';
grid.className = 'filter-selection-grid cell-border';
grid.hideAttribution = true;
document.querySelector(parentSelector)?.appendChild(grid);
grid.source = createFilterSelectionRows();
return () => grid.remove();
}
// src/components/filter-selection/FilterAdvanced.vue
<template>
<RevoGrid
:theme="isDark ? 'darkCompact' : 'compact'"
:columns="columns"
:source="rows"
:plugins="plugins"
:column-types="columnTypes"
class="filter-selection-grid grow h-full cell-border"
stretch="last"
:filter="filter"
hide-attribution
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { currentThemeVue } from '../composables/useRandomData';
import {
createFilterSelectionColumns,
createFilterSelectionFilter,
createFilterSelectionRows,
filterSelectionColumnTypes,
filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';
const { isDark } = currentThemeVue();
const columns = ref(createFilterSelectionColumns());
const rows = ref(createFilterSelectionRows());
const filter = computed(() => createFilterSelectionFilter());
const plugins = filterSelectionPlugins;
const columnTypes = filterSelectionColumnTypes;
</script>
// src/components/filter-selection/FilterAdvanced.tsx
import { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
createFilterSelectionColumns,
createFilterSelectionFilter,
createFilterSelectionRows,
filterSelectionColumnTypes,
filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';
function FilterAdvanced() {
const { isDark } = currentTheme();
const source = useMemo(() => createFilterSelectionRows(), []);
const columns = useMemo(() => createFilterSelectionColumns(), []);
const filter = useMemo(() => createFilterSelectionFilter(), []);
const plugins = useMemo(() => filterSelectionPlugins, []);
const columnTypes = useMemo(() => filterSelectionColumnTypes, []);
return (
<RevoGrid
plugins={plugins}
columns={columns}
source={source}
columnTypes={columnTypes}
className="filter-selection-grid grow h-full cell-border"
hide-attribution
filter={filter}
stretch="last"
theme={isDark() ? 'darkCompact' : 'compact'}
/>
);
}
export default FilterAdvanced;
// src/components/filter-selection/FilterAdvancedAngular.ts
import { Component, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
createFilterSelectionColumns,
createFilterSelectionFilter,
createFilterSelectionRows,
filterSelectionColumnTypes,
filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';
@Component({
selector: 'filter-selection-grid',
standalone: true,
imports: [RevoGrid],
template: `
<revo-grid
[plugins]="plugins"
[columns]="columns"
[source]="source"
[columnTypes]="columnTypes"
stretch="last"
[filter]="filter"
[hideAttribution]="true"
[theme]="theme"
class="filter-selection-grid grow h-full cell-border"
></revo-grid>`,
encapsulation: ViewEncapsulation.None,
})
export class FilterSelectionGridComponent {
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
source = createFilterSelectionRows();
columns = createFilterSelectionColumns();
columnTypes = filterSelectionColumnTypes;
filter = createFilterSelectionFilter();
plugins = filterSelectionPlugins;
}
import type { ColumnFilterConfig, ColumnRegular, ColumnTypes } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
AdvanceFilterPlugin,
ColumnDropdown,
ColumnStretchPlugin,
RowOddPlugin,
columnTypeRenderer,
} from '@revolist/revogrid-pro';
type FilterSelectionFruit = {
name: string;
emoji: string;
family: string;
color: string;
availability: AvailabilityValue;
};
type AvailabilityValue = 'In stock' | 'Backorder' | 'Seasonal';
export type FilterSelectionRow = {
id: number;
name: string;
fruitFamily: string;
fruitColor: string;
availability: AvailabilityValue;
price: number;
harvestedAt: string | null;
};
export const filterSelectionPlugins = [
AdvanceFilterPlugin,
ColumnStretchPlugin,
RowOddPlugin,
];
export const filterSelectionColumnTypes: ColumnTypes = {
currency: new NumberColumnType('$0,0.00'),
dropdown: ColumnDropdown,
};
const FRUITS: FilterSelectionFruit[] = [
{ name: 'Apple', emoji: '🍎', family: 'Tree fruit', color: 'Red', availability: 'In stock' },
{ name: 'Pear', emoji: '🍐', family: 'Tree fruit', color: 'Green', availability: 'In stock' },
{ name: 'Peach', emoji: '🍑', family: 'Stone fruit', color: 'Orange', availability: 'Seasonal' },
{ name: 'Cherry', emoji: '🍒', family: 'Stone fruit', color: 'Red', availability: 'Seasonal' },
{ name: 'Orange', emoji: '🍊', family: 'Citrus', color: 'Orange', availability: 'In stock' },
{ name: 'Lemon', emoji: '🍋', family: 'Citrus', color: 'Yellow', availability: 'Backorder' },
{ name: 'Mango', emoji: '🥭', family: 'Tropical', color: 'Orange', availability: 'Backorder' },
{ name: 'Banana', emoji: '🍌', family: 'Tropical', color: 'Yellow', availability: 'In stock' },
{ name: 'Strawberry', emoji: '🍓', family: 'Berries', color: 'Red', availability: 'Seasonal' },
{ name: 'Grapes', emoji: '🍇', family: 'Berries', color: 'Purple', availability: 'Backorder' },
];
const AVAILABILITY_OPTIONS = [
{ value: 'In stock', label: 'In stock', tone: 'stock' },
{ value: 'Backorder', label: 'Backorder', tone: 'backorder' },
{ value: 'Seasonal', label: 'Seasonal', tone: 'seasonal' },
];
function normalizeFruitValue(fruit: FilterSelectionFruit) {
return `${fruit.name} ${fruit.emoji}`.toLowerCase();
}
function availabilityTone(value: unknown) {
const option = AVAILABILITY_OPTIONS.find((item) => item.value === value || item.label === value);
return option?.tone ?? 'stock';
}
export function renderAvailabilityBadge(h: any, value: unknown) {
const label = String(value || 'In stock');
return h(
'span',
{
class: `filter-selection-availability-badge filter-selection-availability-badge--${availabilityTone(label)}`,
},
label,
);
}
function renderFruitLabel(h: any, label: string) {
const [name, emoji = ''] = label.split(' ');
return h('span', { class: 'filter-selection-fruit-option' }, [
h('span', { class: 'filter-selection-fruit-option__emoji' }, emoji),
h('span', { class: 'filter-selection-fruit-option__label' }, name),
]);
}
export function createFilterSelectionRows(count = 80): FilterSelectionRow[] {
const harvestDates = [
'2024-12-01',
'2025-02-03',
'2025-02-03T18:30:00',
'2025-07-14',
'2026-01-05',
null,
];
return Array.from({ length: count }, (_, index) => {
const fruit = FRUITS[index % FRUITS.length];
return {
id: index + 1,
name: `${fruit.name} ${fruit.emoji}`,
fruitFamily: fruit.family,
fruitColor: fruit.color,
availability: fruit.availability,
price: 5 + ((index * 7) % 24),
harvestedAt: harvestDates[index % harvestDates.length],
};
});
}
export function createFilterSelectionColumns(): ColumnRegular[] {
return [
{
name: 'Fruit',
prop: 'name',
size: 220,
filter: ['string', 'selection'],
columnType: 'string',
columnTemplate: columnTypeRenderer,
},
{
name: 'Availability',
prop: 'availability',
size: 180,
filter: ['selection'],
columnType: 'dropdown',
columnTemplate: columnTypeRenderer,
cellTemplate: (h, { value }) => renderAvailabilityBadge(h, value),
dropdown: {
source: AVAILABILITY_OPTIONS,
syncCellTemplate: true,
},
},
{
name: 'Harvest date',
prop: 'harvestedAt',
size: 150,
filter: ['date', 'selection'],
columnType: 'date',
},
{
name: 'Price',
prop: 'price',
size: 110,
filter: 'number',
columnType: 'currency',
columnTemplate: columnTypeRenderer,
},
];
}
export function createFilterSelectionFilter(): {
multiFilterItems: any;
expressions: ColumnFilterConfig['expressions'];
localization: {
captions: Record<string, string>;
};
selection: {
excelMode: 'windows';
sortDirection: 'asc';
grouping: Record<string, { props: string[]; expandedAll: boolean }>;
getItems: Record<string, () => any[]>;
itemTemplate: Record<string, (h: any, props: any) => any>;
syncCellTemplate: Record<string, boolean>;
};
} {
return {
multiFilterItems: {
name: [
{
id: 0,
type: 'selection',
value: new Set([
normalizeFruitValue(FRUITS[7]),
normalizeFruitValue(FRUITS[5]),
]),
relation: 'and',
hidden: true,
},
],
},
expressions: {
enabled: true,
buttonLabel: 'Expression',
placeholder: [
'in ("Apple 🍎", "Orange 🍊")',
'not in ("Banana 🍌", "Lemon 🍋")',
'contains "berry" OR contains "citrus"',
].join('\n'),
},
localization: {
captions: {
expressionButton: 'Expression',
expressionTitle: 'Filter expression',
expressionPlaceholder: 'Type a filter expression',
expressionApply: 'Apply expression',
expressionInvalid: 'Fix the expression before applying.',
selectionApply: 'Apply',
selectionCancel: 'Cancel',
selectionSelectAll: '(Select All)',
selectionSelectAllSearchResults: '(Select All Search Results)',
selectionAddCurrentSelection: 'Add current selection to filter',
selectionBlanks: '(Blanks)',
},
},
selection: {
excelMode: 'windows',
sortDirection: 'asc',
grouping: {
name: {
props: ['fruitFamily', 'fruitColor'],
expandedAll: true,
},
},
getItems: {
name: () =>
FRUITS.map((fruit) => ({
value: normalizeFruitValue(fruit),
label: `${fruit.name} ${fruit.emoji}`,
fruitFamily: fruit.family,
fruitColor: fruit.color,
})),
availability: () => AVAILABILITY_OPTIONS,
},
syncCellTemplate: {
availability: true,
},
itemTemplate: {
name: (h, { label }) => renderFruitLabel(h, label),
},
},
};
}
The example uses the same setup in Vanilla TypeScript, Vue, React, and Angular. Open the Fruit or Availability filter for a flat checklist, or Harvest date for the date hierarchy.
Apply, cancel, and close
Section titled “Apply, cancel, and close”Excel mode is always transactional, even when disableDynamicFiltering is false or omitted:
- Checking values and typing in the mini-filter only changes the popup draft.
- Apply commits the draft and closes the popup.
- Cancel, Escape, outside click, the close button, and switching to another popup discard the draft.
- Replacing the grid source or filter configuration while the popup is open also discards its draft.
- Pressing Enter in the mini-filter applies the current search result and closes the popup.
The popup is checklist-only; it does not show the normal operator dropdown. Use the standard Selection Filter when changes should apply immediately or users need the ordinary filter-family workflow.

Rows remain unchanged while the checklist is edited and update only after Apply.
Search and Select All
Section titled “Search and Select All”Typing a non-empty mini-filter selects the matching results and changes the parent action to Select All Search Results. Applying immediately keeps the matches and discards selections hidden by the search.
Enable Add current selection to filter when the result should instead be the union of the last applied selection and the current matches. Clearing the search returns the checklist to the last applied state.
Select All affects the currently visible or searched options. Parent checkboxes show a mixed state when only some of their leaf values are selected.

Search selects its matches; Add current selection preserves the previously applied values as well.
Clearing the condition
Section titled “Clearing the condition”Checked values are included. Internally, the selection filter stores unchecked values in an excluded-value Set. Applying either of these boundary states removes the selection condition:
- every available value is checked
- every available value is unchecked
This normalization avoids leaving a hidden condition that represents the complete option set.
Blanks and option sources
Section titled “Blanks and option sources”Empty strings, null, and other normalized blank options share the (Blanks) item. It remains searchable and sorts after nonblank options in Excel mode.
By default, options are derived from the current grid rows and account for compatible filters on other columns. An explicit synchronous or asynchronous selection.getItems is authoritative:
grid.filter = { selection: { excelMode: 'windows', getItems: { status: async () => { const response = await fetch('/api/filter-options/status'); return response.json(); // [{ value: 'open', label: 'Open' }, ...] }, }, },};Keep each value aligned with the normalized value used for row comparison. Custom item templates, grouping, nested-grid settings, parsed cell values, and cascading options work as described in the general Selection Filter guide.
Date hierarchy
Section titled “Date hierarchy”Excel mode creates a local-calendar Year → Month → Day tree only when the column provides all three signals:
grid.columns = [ { name: 'Created', prop: 'createdAt', columnType: 'date', filter: ['date', 'selection'], },];The hierarchy starts collapsed and uses browser-locale month names. Multiple timestamps on the same local day share one day node. Parent and Select All checkboxes derive their mixed state from the raw leaf values. Invalid nonblank values remain selectable as flat root options, followed by (Blanks).

The hierarchy groups raw values by their browser-local calendar day while preserving each raw leaf for filtering.
Setting only columnType: 'date', or only including date in filter, is not enough. Without the full configuration, the column uses a flat selection list.
Restore applied selection
Section titled “Restore applied selection”Restore the same state model used by the default selection filter. The Set contains normalized values that should be unchecked:
grid.filter = { multiFilterItems: { status: [ { id: 1, type: 'selection', value: new Set(['closed', 'archived']), relation: 'and', hidden: true, }, ], }, selection: { excelMode: 'windows', },};Do not replace the full filter object with only multiFilterItems when the grid also uses localization or selection settings. Build the complete configuration or use the active plugin APIs described in Filter State and Presets.
Localize the checklist
Section titled “Localize the checklist”Override only the captions the application needs:
import { defineAdvancedFilterConfig } from '@revolist/revogrid-pro';
grid.filter = defineAdvancedFilterConfig({ localization: { captions: { selectionSearchPlaceholder: 'Search values...', selectionApply: 'Use values', selectionCancel: 'Discard', selectionSelectAll: '(All values)', selectionSelectAllSearchResults: '(All search results)', selectionAddCurrentSelection: 'Keep the current selection too', selectionBlanks: '(Empty)', }, }, selection: { excelMode: 'windows', },});defineAdvancedFilterConfig fills the remaining default filter names and captions.
Common mistakes
Section titled “Common mistakes”- Expecting Excel mode on a column that does not include the
selectionfilter. - Treating the mini-filter as a live grid search; its draft is committed only by Apply or Enter.
- Expecting
disableDynamicFilteringto remove Apply and Cancel from Excel mode. - Saving
multiFilterItemsdirectly as JSON without converting selectionSetvalues. - Configuring only one of the two required date signals and expecting a hierarchy.
- Returning display labels from
getItemsthat do not map to the values compared with row data.