Index filters
Use index filters to allow merchants to filter, search, and sort their index table data and create unique saved views from the results.
Index filters component examples
An IndexFilters component with view management, search, filtering, and sorting.
import {
TextField,
IndexTable,
LegacyCard,
IndexFilters,
useSetIndexFiltersMode,
useIndexResourceState,
Text,
ChoiceList,
RangeSlider,
Badge,
} from '@shopify/polaris';
import type {IndexFiltersProps, TabProps} from '@shopify/polaris';
import {useState, useCallback} from 'react';
function IndexFiltersDefaultExample() {
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
const [itemStrings, setItemStrings] = useState([
'All',
'Unpaid',
'Open',
'Closed',
'Local delivery',
'Local pickup',
]);
const deleteView = (index: number) => {
const newItemStrings = [...itemStrings];
newItemStrings.splice(index, 1);
setItemStrings(newItemStrings);
setSelected(0);
};
const duplicateView = async (name: string) => {
setItemStrings([...itemStrings, name]);
setSelected(itemStrings.length);
await sleep(1);
return true;
};
const tabs: TabProps[] = itemStrings.map((item, index) => ({
content: item,
index,
onAction: () => {},
id: `${item}-${index}`,
isLocked: index === 0,
actions:
index === 0
? []
: [
{
type: 'rename',
onAction: () => {},
onPrimaryAction: async (value: string): Promise<boolean> => {
const newItemsStrings = tabs.map((item, idx) => {
if (idx === index) {
return value;
}
return item.content;
});
await sleep(1);
setItemStrings(newItemsStrings);
return true;
},
},
{
type: 'duplicate',
onPrimaryAction: async (value: string): Promise<boolean> => {
await sleep(1);
duplicateView(value);
return true;
},
},
{
type: 'edit',
},
{
type: 'delete',
onPrimaryAction: async () => {
await sleep(1);
deleteView(index);
return true;
},
},
],
}));
const [selected, setSelected] = useState(0);
const onCreateNewView = async (value: string) => {
await sleep(500);
setItemStrings([...itemStrings, value]);
setSelected(itemStrings.length);
return true;
};
const sortOptions: IndexFiltersProps['sortOptions'] = [
{label: 'Order', value: 'order asc', directionLabel: 'Ascending'},
{label: 'Order', value: 'order desc', directionLabel: 'Descending'},
{label: 'Customer', value: 'customer asc', directionLabel: 'A-Z'},
{label: 'Customer', value: 'customer desc', directionLabel: 'Z-A'},
{label: 'Date', value: 'date asc', directionLabel: 'A-Z'},
{label: 'Date', value: 'date desc', directionLabel: 'Z-A'},
{label: 'Total', value: 'total asc', directionLabel: 'Ascending'},
{label: 'Total', value: 'total desc', directionLabel: 'Descending'},
];
const [sortSelected, setSortSelected] = useState(['order asc']);
const {mode, setMode} = useSetIndexFiltersMode();
const onHandleCancel = () => {};
const onHandleSave = async () => {
await sleep(1);
return true;
};
const primaryAction: IndexFiltersProps['primaryAction'] =
selected === 0
? {
type: 'save-as',
onAction: onCreateNewView,
disabled: false,
loading: false,
}
: {
type: 'save',
onAction: onHandleSave,
disabled: false,
loading: false,
};
const [accountStatus, setAccountStatus] = useState<string[] | undefined>(
undefined,
);
const [moneySpent, setMoneySpent] = useState<[number, number] | undefined>(
undefined,
);
const [taggedWith, setTaggedWith] = useState('');
const [queryValue, setQueryValue] = useState('');
const handleAccountStatusChange = useCallback(
(value: string[]) => setAccountStatus(value),
[],
);
const handleMoneySpentChange = useCallback(
(value: [number, number]) => setMoneySpent(value),
[],
);
const handleTaggedWithChange = useCallback(
(value: string) => setTaggedWith(value),
[],
);
const handleFiltersQueryChange = useCallback(
(value: string) => setQueryValue(value),
[],
);
const handleAccountStatusRemove = useCallback(
() => setAccountStatus(undefined),
[],
);
const handleMoneySpentRemove = useCallback(
() => setMoneySpent(undefined),
[],
);
const handleTaggedWithRemove = useCallback(() => setTaggedWith(''), []);
const handleQueryValueRemove = useCallback(() => setQueryValue(''), []);
const handleFiltersClearAll = useCallback(() => {
handleAccountStatusRemove();
handleMoneySpentRemove();
handleTaggedWithRemove();
handleQueryValueRemove();
}, [
handleAccountStatusRemove,
handleMoneySpentRemove,
handleQueryValueRemove,
handleTaggedWithRemove,
]);
const filters = [
{
key: 'accountStatus',
label: 'Account status',
filter: (
<ChoiceList
title="Account status"
titleHidden
choices={[
{label: 'Enabled', value: 'enabled'},
{label: 'Not invited', value: 'not invited'},
{label: 'Invited', value: 'invited'},
{label: 'Declined', value: 'declined'},
]}
selected={accountStatus || []}
onChange={handleAccountStatusChange}
allowMultiple
/>
),
shortcut: true,
},
{
key: 'taggedWith',
label: 'Tagged with',
filter: (
<TextField
label="Tagged with"
value={taggedWith}
onChange={handleTaggedWithChange}
autoComplete="off"
labelHidden
/>
),
shortcut: true,
},
{
key: 'moneySpent',
label: 'Money spent',
filter: (
<RangeSlider
label="Money spent is between"
labelHidden
value={moneySpent || [0, 500]}
prefix="$"
output
min={0}
max={2000}
step={1}
onChange={handleMoneySpentChange}
/>
),
},
];
const appliedFilters: IndexFiltersProps['appliedFilters'] = [];
if (accountStatus && !isEmpty(accountStatus)) {
const key = 'accountStatus';
appliedFilters.push({
key,
label: disambiguateLabel(key, accountStatus),
onRemove: handleAccountStatusRemove,
});
}
if (moneySpent) {
const key = 'moneySpent';
appliedFilters.push({
key,
label: disambiguateLabel(key, moneySpent),
onRemove: handleMoneySpentRemove,
});
}
if (!isEmpty(taggedWith)) {
const key = 'taggedWith';
appliedFilters.push({
key,
label: disambiguateLabel(key, taggedWith),
onRemove: handleTaggedWithRemove,
});
}
const orders = [
{
id: '1020',
order: (
<Text as="span" variant="bodyMd" fontWeight="semibold">
#1020
</Text>
),
date: 'Jul 20 at 4:34pm',
customer: 'Jaydon Stanton',
total: '$969.44',
paymentStatus: <Badge progress="complete">Paid</Badge>,
fulfillmentStatus: <Badge progress="incomplete">Unfulfilled</Badge>,
},
{
id: '1019',
order: (
<Text as="span" variant="bodyMd" fontWeight="semibold">
#1019
</Text>
),
date: 'Jul 20 at 3:46pm',
customer: 'Ruben Westerfelt',
total: '$701.19',
paymentStatus: <Badge progress="partiallyComplete">Partially paid</Badge>,
fulfillmentStatus: <Badge progress="incomplete">Unfulfilled</Badge>,
},
{
id: '1018',
order: (
<Text as="span" variant="bodyMd" fontWeight="semibold">
#1018
</Text>
),
date: 'Jul 20 at 3.44pm',
customer: 'Leo Carder',
total: '$798.24',
paymentStatus: <Badge progress="complete">Paid</Badge>,
fulfillmentStatus: <Badge progress="incomplete">Unfulfilled</Badge>,
},
];
const resourceName = {
singular: 'order',
plural: 'orders',
};
const {selectedResources, allResourcesSelected, handleSelectionChange} =
useIndexResourceState(orders);
const rowMarkup = orders.map(
(
{id, order, date, customer, total, paymentStatus, fulfillmentStatus},
index,
) => (
<IndexTable.Row
id={id}
key={id}
selected={selectedResources.includes(id)}
position={index}
>
<IndexTable.Cell>
<Text variant="bodyMd" fontWeight="bold" as="span">
{order}
</Text>
</IndexTable.Cell>
<IndexTable.Cell>{date}</IndexTable.Cell>
<IndexTable.Cell>{customer}</IndexTable.Cell>
<IndexTable.Cell>
<Text as="span" alignment="end" numeric>
{total}
</Text>
</IndexTable.Cell>
<IndexTable.Cell>{paymentStatus}</IndexTable.Cell>
<IndexTable.Cell>{fulfillmentStatus}</IndexTable.Cell>
</IndexTable.Row>
),
);
return (
<LegacyCard>
<IndexFilters
sortOptions={sortOptions}
sortSelected={sortSelected}
queryValue={queryValue}
queryPlaceholder="Searching in all"
onQueryChange={handleFiltersQueryChange}
onQueryClear={() => setQueryValue('')}
onSort={setSortSelected}
primaryAction={primaryAction}
cancelAction={{
onAction: onHandleCancel,
disabled: false,
loading: false,
}}
tabs={tabs}
selected={selected}
onSelect={setSelected}
canCreateNewView
onCreateNewView={onCreateNewView}
filters={filters}
appliedFilters={appliedFilters}
onClearAll={handleFiltersClearAll}
mode={mode}
setMode={setMode}
/>
<IndexTable
resourceName={resourceName}
itemCount={orders.length}
selectedItemsCount={
allResourcesSelected ? 'All' : selectedResources.length
}
onSelectionChange={handleSelectionChange}
headings={[
{title: 'Order'},
{title: 'Date'},
{title: 'Customer'},
{title: 'Total', alignment: 'end'},
{title: 'Payment status'},
{title: 'Fulfillment status'},
]}
>
{rowMarkup}
</IndexTable>
</LegacyCard>
);
function disambiguateLabel(key: string, value: string | any[]): string {
switch (key) {
case 'moneySpent':
return `Money spent is between $${value[0]} and $${value[1]}`;
case 'taggedWith':
return `Tagged with ${value}`;
case 'accountStatus':
return (value as string[]).map((val) => `Customer ${val}`).join(', ');
default:
return value as string;
}
}
function isEmpty(value: string | any[]) {
if (Array.isArray(value)) {
return value.length === 0;
} else {
return value === '' || value == null;
}
}
}
Props
- sortOptions?[]
The available sorting choices. If not present, the sort button will not show.
- sortSelected?string[]
The currently selected sort choice. Required if using sorting.
- onSort?(value: string[]) => void
Optional callback invoked when a merchant changes the sort order. Required if using sorting.
- onSortKeyChange?(value: string) => void
Optional callback when using saved views and changing the sort key.
- onSortDirectionChange?(value: string) => void
Optional callback when using saved views and changing the sort direction.
- onAddFilterClick?() => void
Callback when the add filter button is clicked, to be passed to AlphaFilters.
- primaryAction?
The primary action to display.
- cancelAction?
The cancel action to display.
- onEditStart?(mode: Exclude< , .Default >) => void
Optional callback invoked when a merchant begins to edit a view.
- mode
The current mode of the IndexFilters component. Used to determine which view to show.
- disclosureZIndexOverride?number
Override z-index of popovers and tooltips.
- setMode(mode: ) => void
Callback to set the mode of the IndexFilters component.
- disabled?boolean
Will disable all the elements within the IndexFilters component.
- disableQueryField?boolean
Will disable just the query field.
- disableStickyMode?boolean
If true, the sticky interaction on smaller devices will be disabled.
- isFlushWhenSticky?boolean
If the component should go flush to the top of the page when sticking.
- canCreateNewView?boolean
Whether the index supports creating new views.
- onCreateNewView?(name: string) => Promise<boolean>
Callback invoked when a merchant creates a new view.
- filteringAccessibilityLabel?string
Optional override to the default aria-label for the button that toggles the filtering mode.
- filteringAccessibilityTooltip?string
Optional override to the default Tooltip message for the button that toggles the filtering mode.
- closeOnChildOverlayClick?boolean
Whether the filter should close when clicking inside another Popover.
- disableKeyboardShortcuts?boolean
Optional override to the default keyboard shortcuts available. Should be set to true for all instances of this component not controlling a root-level index.
- showEditColumnsButton?boolean
Whether to display the edit columns button with the other default mode filter actions.
- autoFocusSearchField?boolean
Whether or not to auto-focus the search field when it renders.
Merchants use filters to:
- Create different subsets of list items
- Search list items by typing a query into the text input
- Sort list items by column
The way that merchants interact with index filters depends on the components that you decide to incorporate. It supports configuration of a search query input, sorting options, and one or more filters that can be made up of different inputs.
Merchants use the tabs in index tables to:
- Control which view is visible
- Edit the applied filters and search terms of a view
- Create, rename, duplicate, or delete views
You can create views and control which actions can be performed on a particular view.
Anatomy
Index filters are made up of the following:
- Tabs: A list of saved views. Each tab represents a subset of the list that has been sorted, filtered, and or queried and saved with a unique name. New views can be created directly from the tab list, or by editing the filters, query, or sort selection of an existing view and saving it as new.
- Search and filter, and sort buttons: The search and filter button allows merchants to toggle the index table from "View" mode to "Filter" mode. When clicked, the button reveals the search field and the filters that allow merchants to edit or create saved views. The sort button activates a popover displaying a list of options merchants can choose from to sort the list items. Merchants can also choose whether the list should be sorted in ascending or descending order.
- Filters: A set of useful ways to narrow down the list based on the common actions merchants may need to take on the data. The filters should present merchants with form inputs that help them include or exclude list items from the view based on their data.
- Action buttons: Primary and secondary actions that a merchant can take on the current view. The primary action will always be either "Save" or "Save as" depending on whether the view is mutable, and the secondary action will always be "Cancel".
Accessibility
The filters component relies on the accessibility features of several other components:
Maintain accessibility with custom features
Since custom HTML can be passed to the component for additional actions, ensure that the filtering system you build is accessible as a whole.
All merchants must be able to:
- Identify and understand the labels of all controls
- Be notified of state changes as they use the filter controls
- Complete all actions using a keyboard
Best practices
Index filters should:
- Reduce merchant effort by promoting the filtering categories that are most commonly used
- Include no more than 2 or 3 promoted filters
- Consider small screen sizes when designing the interface for each filter and the total number filters to include
- Use children only for content that’s related or relevant to filtering
Content guidelines
Text field
The text field should be clearly labeled so it’s obvious to merchants what they should enter into the field.
- Filter orders
- Enter text here
Filter badges
Use the name of the filter if the purpose of the name is clear on its own. For example, when you see a filter badge that reads Fulfilled, it’s intuitive that it falls under the Fulfillment status category.
- Fulfilled, Unfulfilled
- Fulfillment: Fulfilled, Unfulfilled
If the filter name is ambiguous on its own, add a descriptive word related to the status. For example, Low doesn’t make sense out of context. Add the word “risk” so that merchants know it’s from the Risk category.
- High risk, Low risk
- High, Low
Group tags from the same category together.
- (Unfulfilled, Fulfilled)
- (Unfulfilled) (fulfilled)
If all tag pills selected: truncate in the middle
- Paid, par… unpaid
- All payment status filters selected, Paid, unpa…