Skip to contentSkip to navigationSkip to topbar
Paste assistant Assistant
Figma
Star

Filter

Filters enable users to narrow down and refine results across various types of content pages, including rich content pages, data tables, lists, data insights, and more.


a demo page with filter pattern
// import the components for filter group patterns as required

import { FormPillGroup, useFormPillState } from "@twilio-paste/core/form-pill-group";
import {Input} from '@twilio-paste/core/input';
import {MultiselectCombobox, useMultiselectCombobox} from "@twilio-paste/core/combobox";
import {Button} from '@twilio-paste/core/button';
import {ButtonGroup} from "@twilio-paste/core/button-group";
import {Disclosure, DisclosureContent, DisclosureHeading} from "@twilio-paste/core/disclosure";
import {Badge} from "@twilio-paste/core/badge";
import {Checkbox, CheckboxGroup} from "@twilio-paste/core/checkbox";
import {Radio, RadioGroup} from "@twilio-paste/core/radio-group";
import { Popover, PopoverButton, PopoverContainer, usePopoverState } from "@twilio-paste/core/popover";

Usage

Usage page anchor

The filter pattern should be used when a user is presented with a page containing a large amount of data that they could be filtering or searching through. The filter parameters and functionality you can surface to the user will be highly dependent on the page use case.

This pattern provides recommendations for selection methods, layout, and behavior to reduce complexity and improve user efficiency when using different types of filters in a feature. Before adding filters to your feature, it's essential to:

  1. Familiarize yourself with your feature’s filter values and categories.
    • List all filtering criteria available.
    • Map out the relationships between different filters and choose the ones that will be part of your feature. Remember filters should give users a sense of control and not overwhelm them.
    • A truly usable faceted search provides filter categories and filter values that are appropriate, predictable, free of jargon, and prioritized.

      Nielsen Norman Group, Defining Helpful Filter Categories and Values for Better UX(link takes you to an external page)

      The filter category label should be the same as the form field label
    • Check our Form documentation to choose the best selection method for your filter use case.
  2. Determine users' intent and their relationship with the product and dataset.
    • Categorize filters into the most relevant and commonly used filters versus the more advanced or less used ones.
    • Determine how many different types of users will access the feature and how much the filtering criteria will vary from use case to use case. If necessary, consider allowing users to add custom filters to provide a more tailored experience.
    • Identify any filter values that naturally lead to additional refinement options, and consider implementing conditional filters.
  3. Determine any technical constraints.
    • Ensure you have a good understanding of how the data is set up, including which filtering behaviors and combinations are not possible.
    • Evaluate carefully the best filtering behavior for fetching results. Keep in mind user intent, but also loading times and the size of the dataset.
    • Ensure that all filter options correspond to data present in the results. Avoid including filters for data that are not visible or accessible within the dataset.
    • Evaluate the expected growth of your filtering criteria and ensure it can manage larger datasets without notable performance decline.

Accessibility

Accessibility page anchor

When using the filter pattern, ensure that users:

  • Can easily identify and understand labels for each selection method, filter value, or category.
  • Are notified of state changes. For example, when a button changes from the disabled state to the default state, or when the dataset enters a loading state.
  • Can complete all actions with the keyboard.
(information)

Check accessibility considerations for each component part of the filter feature, for example if you’re using Time Picker, follow its accessibility guidelines.

A filter bar is the default way of displaying filters.

Use it when all filters are relevant and necessary to the search. It can also be used in combination with a More filters Side Panel when there are secondary filter criteria.

Ingredients

Ingredients page anchor

export const FilterPatternExample = ({data}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  const pillState = useFormPillState();
  const popover = usePopoverState({ baseId: pill });
  return (
    <>
      <Heading as="h1" variant="heading50">Filter</Heading>
      <FormPillGroup {...pillState} aria-label="Filters:" size="large">
        {filterList.map((pill) => {
          return (
            <PopoverContainer key={pill} state={popover}>
              <PopoverButton
                variant="reset"
                size="reset"
                borderRadius="borderRadiusPill"
              >
                <FormPill
                  {...pillState}
                  selected={isSelected}
                  onDismiss={
                    // Remove the filter from the selected filters
                  }
                >
                  {!isSelected ? <PlusIcon decorative /> : null}
                  <FilterPillView label={filterMap[pill].label} selectedType={isSelected ? pill : null} selectedValue={value} />
                </FormPill>
              </PopoverButton>
              <Popover aria-label={pill} width="size40">
                <FilterComponent value={value} onApply={onApply} popover={popover} onRemove={onRemove} /> // Create different filter components
              </Popover>
            </PopoverContainer>
          );
        })}
      </FormPillGroup>
      <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space30" marginTop="space50">
        <Box display="flex" columnGap="space30">
          <DetailText marginTop="space0">
            <Text as="span" color="colorTextWeak" fontSize="fontSize30">
              {filteredTableData.length} result{filteredTableData.length !== 1 && "s"}
            </Text>
          </DetailText>
          {filteredTableData.length !== data.length ? (
            <Button variant="link" onClick={handleClearAll}>
              Clear all
            </Button>
          ) : null}
        </Box>
      </Box>
      <DataGrid data={filteredTableData} />
    </>
  )
}
  • When a filter value is selected by the user, the corresponding Form Pill will enter a selected state.
  • When a single filter value is selected, the filter updates to display the name of the selected filter.
  • When two or more filter values are selected per filter category, the total number of selected filters will be displayed in a counter badge.
  • In some scenarios, it is necessary to have filter values pre-selected by default to display results. Since the selection was not made by the user, the Form Pill will remain in its default state, displaying the pre-selected filter value.
The filter bar has different states based on interaction
Do

Arrange filters in a way that mirrors how users think about and interact with the data.

Don't

Don't overload the filter bar with too many filters. It's designed for prioritized, essential filters to keep the screen uncluttered and easy to navigate.

Do

Arrange filter values in a logical order; for example, dates should be in chronological order and names should be in alphabetical order.

Use it in scenarios where the list of filters is extensive and there are filters that are not a priority to the search. Prioritize the most relevant and commonly used filters in the filter bar for visibility, while offering advanced filtering options in a Side Panel.


export const MoreFilterPatternExample = ({data, filterList}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const pillState = useFormPillState();
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  return (
    <>
      <SidePanelContainer id={sidePanelId} isOpen={isOpen} setIsOpen={setIsOpen}>
        <SidePanel>
          <SidePanelHeader>
            <Heading as="h3" variant="heading30">
              More filters
            </Heading>
          </SidePanelHeader>
          <Separator orientation="horizontal" verticalSpacing="space0" />
          <SidePanelBody>
            <Box
              display="flex"
              flexDirection="column"
              rowGap="space40"
              marginTop="space70"
              marginBottom="space70"
              width="100%"
            >
              {filterList.map((filter) => {
                return (
                <Disclosure>
                  <DisclosureHeading as="h2" variant="heading50">
                    <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space20" width="100%">
                      <Box as="span">{filter.label}</Box>
                        {selectedCount ? (
                          <Badge as="span" variant="neutral_counter" size="small">
                            Selected {filter.type === "status" ? 1 : selectedCount}
                          </Badge>
                        ) : null}
                      </Box>
                  </DisclosureHeading>
                  <DisclosureContent>
                    <FilterComponent
                      key={filter.label}
                      label={filter.label}
                      items={filter.items}
                      setSelectedCount={setSelectedCount}
                      setSelectedMoreFilters={setSelectedMoreFilters}
                      selectedMoreFilters={tempSelectedMoreFilters}
                    />
                  </DisclosureContent>
                </Disclosure>
                );
              })}
            </Box>
          </SidePanelBody>
          <SidePanelFooter>
            <ButtonGroup>
              <Button
                variant="primary"
                onClick={() => {
                  // Apply filters
                }}
              >
                Apply
              </Button>
              <Button
                variant="secondary"
                onClick={() => {
                  // Clear all filters
                }}
              >
                Clear all
              </Button>
            </ButtonGroup>
          </SidePanelFooter>
        </SidePanel>
        <SidePanelPushContentWrapper>
        // Filter components from other examples
        </SidePanelPushContentWrapper>
      </SidePanelContainer>
    </>
  )
}
  • In the “More filters” Side Panel when filters are selected, a counter Badge will be displayed in the right corner of the filter value (disclosure)
  • “More filters” Button will have a counter Badge with the number of selected filter values.
Do

Ensure that the filters in the Filter bar and those in the Side Panel are independent and won’t need to be applied along with another filter criteria to return relevant results.

Don't

Don’t use a “More Filters” Side Panel for fewer than 3 additional filters.

An "Add filters" Popover allows users to add a filter value from a predefined list of filter options. Use it in scenarios where the list of filters may not be relevant to all users, or the user would benefit from creating their own filters set.


export const AddFilterPatternExample = ({data, filterList}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const pillState = useFormPillState();
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  React.useEffect(() => {
    const newFilters = { ...selectedFilters };
    for (const key in selectedFilters) {
      const typedKey = key as FilterListType[0];
      if (!addedFilters.includes(typedKey) && addFiltersList?.includes(typedKey)) {
        delete newFilters[key];
      }
    }
    setSelectedFilters(newFilters);
    handleApplyFilters(newFilters as selectedFilterProps);
  }, [addedFilters, addFiltersList]);
  function removeFilter(filter: string): void {
    const newFilters = { ...selectedFilters };
    const { [filter]: _, ...rest } = newFilters;
    setSelectedFilters(rest);
    handleApplyFilters(rest as selectedFilterProps);
  }
  return (
    <>
      <Heading as="h1" variant="heading50">Filter</Heading>
      <FormPillGroup {...pillState} aria-label="Filters:" size="large">
        {filterList.map((pill) => {
          return (
            <PopoverContainer key={pill} state={popover}>
              <PopoverButton
                variant="reset"
                size="reset"
                borderRadius="borderRadiusPill"
              >
                <FormPill
                  {...pillState}
                  selected={isSelected}
                  onDismiss={
                    // Remove the filter from the selected filters
                  }
                >
                  {!isSelected ? <PlusIcon decorative /> : null}
                  <FilterPillView label={filterMap[pill].label} selectedType={isSelected ? pill : null} selectedValue={value} />
                </FormPill>
              </PopoverButton>
              <Popover aria-label={pill} width="size40">
                <FilterComponent value={value} onApply={onApply} popover={popover} onRemove={onRemove} /> // Create different filter components
              </Popover>
            </PopoverContainer>
          );
        })}
        {addedFilters.length > 0
          ? addedFilters.map((pill: string) => {
              return (
                <FilterPill
                  key={pill}
                  pill={pill}
                  selectedFilters={selectedFilters}
                  filterMap={filterMap}
                  pillState={pillState}
                  onDismiss={() => {
                    removeFilter(pill);
                  }}
                  onApply={(type: string, value) => {
                    if (!value || (Array.isArray(value) && value.length === 0)) {
                      removeFilter(type);
                      return;
                    }
                    addFilter(type, value);
                  }}
                  onRemove={() => {
                    const newFilters = addedFilters.filter((item) => item !== pill);
                    setAddedFilters(newFilters);
                    removeFilter(pill);
                  }}
                />
              );
            })
          : null}
        {addFiltersList && addFiltersList.length > 0 ? (
          <AddFilters
            onApply={(_: string, addFilterSelectedList) => {
              const sluggedList = (addFilterSelectedList as FilterListType).map((item) => slugify(item));
              setAddedFilters(sluggedList as FilterListType);
            }}
            addFiltersList={addFiltersList}
            filterMap={filterMap}
            recommendedFiltersList={recommendedFiltersList}
            value={addedFilters}
          />
        ) : null}
      </FormPillGroup>
      <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space30" marginTop="space50">
        <Box display="flex" columnGap="space30">
          <DetailText marginTop="space0">
            <Text as="span" color="colorTextWeak" fontSize="fontSize30">
              {filteredTableData.length} result{filteredTableData.length !== 1 && "s"}
            </Text>
          </DetailText>
          {filteredTableData.length !== data.length ? (
            <Button variant="link" onClick={handleClearAll}>
              Clear all
            </Button>
          ) : null}
        </Box>
      </Box>
      <DataGrid data={filteredTableData} />
    </>
  )
}
  • Selected filters will be added to the filter bar where they will become functional.
  • Added filters behave as a normal filter with the only difference being they can be removed.
    • Added filters can be removed through the “Add filters” Popover or through the specific filter’s Popover.
Do

Limit the primary display to max 3 prioritized filters, and offer the option to add custom filters. This behavior is designed to provide users with more choice and not overwhelm them with excessive always visible options.

Don't

When your use case includes a "more filters" panel, avoid prompting users to add more filters, as the existing filter selection should already meet their filtering needs.

Combining search and filters

Combining search and filters page anchor

If there is search functionality, it should always take priority over filters. The search should be placed higher in the hierarchy, with the expectation that users will search first and then use the filters to refine and narrow down the search results.


export const SearchFilterPatternExample = ({data}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const pillState = useFormPillState();
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  return (
    <>
      <Heading as="h1" variant="heading50">Filter</Heading>
      <>
        <Label htmlFor="search-filter">Search</Label>
        <Input
          id="search-filter"
          type="search"
          insertBefore={<SearchIcon decorative />}
          name="search-filter"
          onChange={onChange}
        />
      </>
      <FormPillGroup {...pillState} aria-label="Filters:" size="large">
        {filterList.map((pill) => {
          return (
            <PopoverContainer key={pill} state={popover}>
              <PopoverButton
                variant="reset"
                size="reset"
                borderRadius="borderRadiusPill"
              >
                <FormPill
                  {...pillState}
                  selected={isSelected}
                  onDismiss={
                    // Remove the filter from the selected filters
                  }
                >
                  {!isSelected ? <PlusIcon decorative /> : null}
                  <FilterPillView label={filterMap[pill].label} selectedType={isSelected ? pill : null} selectedValue={value} />
                </FormPill>
              </PopoverButton>
              <Popover aria-label={pill} width="size40">
                <FilterComponent value={value} onApply={onApply} popover={popover} onRemove={onRemove} /> // Create different filter components
              </Popover>
            </PopoverContainer>
          );
        })}
      </FormPillGroup>
      <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space30" marginTop="space50">
        <Box display="flex" columnGap="space30">
          <DetailText marginTop="space0">
            <Text as="span" color="colorTextWeak" fontSize="fontSize30">
              {filteredTableData.length} result{filteredTableData.length !== 1 && "s"}
            </Text>
          </DetailText>
          {filteredTableData.length !== data.length ? (
            <Button variant="link" onClick={handleClearAll}>
              Clear all
            </Button>
          ) : null}
        </Box>
      </Box>
      <DataGrid data={filteredTableData} />
    </>
  )
}

A conditional filter is a type of filter that becomes available based on the initial filter selections made by the user.


export const ConditionalFilterPatternExample = ({data}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const pillState = useFormPillState();
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  return (
    <>
      <Heading as="h1" variant="heading50">Filter</Heading>
      <FormPillGroup {...pillState} aria-label="Filters:" size="large">
        {filterList.map((pill) => {
          return (
            <PopoverContainer key={pill} state={popover}>
              <PopoverButton
                variant="reset"
                size="reset"
                borderRadius="borderRadiusPill"
              >
                <FormPill
                  {...pillState}
                  selected={isSelected}
                  onDismiss={
                    // Remove the filter from the selected filters
                  }
                >
                  {!isSelected ? <PlusIcon decorative /> : null}
                  <FilterPillView label={filterMap[pill].label} selectedType={isSelected ? pill : null} selectedValue={value} />
                </FormPill>
              </PopoverButton>
              <Popover aria-label={pill} width="size40">
                <FilterComponent value={value} onApply={onApply} popover={popover} onRemove={onRemove} /> // Create different filter components
              </Popover>
            </PopoverContainer>
          );
        })}
      </FormPillGroup>
      <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space30" marginTop="space50">
        <Box display="flex" columnGap="space30">
          <DetailText marginTop="space0">
            <Text as="span" color="colorTextWeak" fontSize="fontSize30">
              {filteredTableData.length} result{filteredTableData.length !== 1 && "s"}
            </Text>
          </DetailText>
          {filteredTableData.length !== data.length ? (
            <Button variant="link" onClick={handleClearAll}>
              Clear all
            </Button>
          ) : null}
        </Box>
      </Box>
      <DataGrid data={filteredTableData} />
    </>
  )
}

Use it when filter selections naturally lead to additional criteria, or where certain filter formulas are only necessary for very specific use cases.

Do

Consider the performance implications of loading conditional filters.

Don't

Don’t use conditional filtering for essential filter criteria.

Batch filtering requires the user to click an “Apply” button to see the results.

The filter bar supports batch filtering of results on click of apply

Use batch filtering when:

  • There are categories with multiple interdependent filter values, and the user might want to take more time selecting the right group of filters.
  • The dataset cannot be refreshed automatically and the system needs time to load the data.
  • You want to prevent "no results" scenarios.

To load results: Once the user clicks the "Apply" button, the Popover will automatically close, and a loading screen will be displayed until the data is fully loaded. It is recommended to use the Skeleton Loader when loading the results of the filters and/or search.

Dynamic filters are applied as soon as a filter selection is made.

The filter bar also supports batch filtering of results as soon as selection is made

Use dynamic filtering when:

  • The filter experience is more explorative and users need to play around with the filters to find their desired results.
  • The user is expected to make multiple quick filter changes during the task.
(information)

Be cautious when dynamically updating results, as they can divert user attention.

To load results: Dynamic filtering is designed to allow users to experiment with different filters. The Popover will remain open until the user closes it, while the results load in the background.

When the applied filters and/or search does not return any results, use the empty state pattern to inform the user and provide a method to reset all filter and search criteria.


export const EmptyFilterPatternExample = ({data}): React.ReactNode => {
  const [selectedFilters, setSelectedFilters] = React.useState({});
  const pillState = useFormPillState();
  const [filteredTableData, setFilteredTableData] = React.useState(data);
  return (
    <>
      <Heading as="h1" variant="heading50">Filter</Heading>
      <FormPillGroup {...pillState} aria-label="Filters:" size="large">
        {filterList.map((pill) => {
          return (
            <PopoverContainer key={pill} state={popover}>
              <PopoverButton
                variant="reset"
                size="reset"
                borderRadius="borderRadiusPill"
              >
                <FormPill
                  {...pillState}
                  selected={isSelected}
                  onDismiss={
                    // Remove the filter from the selected filters
                  }
                >
                  {!isSelected ? <PlusIcon decorative /> : null}
                  <FilterPillView label={filterMap[pill].label} selectedType={isSelected ? pill : null} selectedValue={value} />
                </FormPill>
              </PopoverButton>
              <Popover aria-label={pill} width="size40">
                <FilterComponent value={value} onApply={onApply} popover={popover} onRemove={onRemove} /> // Create different filter components
              </Popover>
            </PopoverContainer>
          );
        })}
      </FormPillGroup>
      <Box display="flex" justifyContent="space-between" alignItems="center" columnGap="space30" marginTop="space50">
        <Box display="flex" columnGap="space30">
          <DetailText marginTop="space0">
            <Text as="span" color="colorTextWeak" fontSize="fontSize30">
              {filteredTableData.length} result{filteredTableData.length !== 1 && "s"}
            </Text>
          </DetailText>
          {filteredTableData.length !== data.length ? (
            <Button variant="link" onClick={handleClearAll}>
              Clear all
            </Button>
          ) : null}
        </Box>
      </Box>
      <DataGrid data={filteredTableData} />
    </>
  )
}

A clear filter option will be available when filters are selected. By default, "clear all filters" will remove all filter criteria and display all results. In use cases where there is a selected filter value by default, "clear all filters" will reset the filters to their default state.

Do

Provide ways to clear all filters as a global action.

Do

Provide ways to clear all filters on a filter category level.

On mobile screen sizes, consolidate all filter options under a "Filter" button to optimize screen space and usability.

On smaller screens, filter modal will take full width

At smaller screen sizes, filter Pills will overflow onto the next row. All other actions (such as "More filters", “Add filters”, "Clear all” and table actions) move to the last row to maintain a clean and organized layout.

filter pills will stack vertically to make better use of available space