Skip to content
lab components / Tables and lists

Filters

Help users quickly and efficiently narrow down data sets within tables or lists.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { useFilterGroup, useSingleFilter } from "@siteimprove/fancylab";

#Examples

Filters help users quickly and efficiently narrow down data sets within tables or lists. They provide control and customization, allowing users to focus on the most relevant information.

#Basic usage with single filter

Allows users to refine data based on one specific criterion. Ideal when there's a single primary way users categorize or sort information.

Use Cases:

  • Filtering a list of modules (e.g., "DCI”, “Accessibility“, “Quality Assurance“).
  • Filtering search results by content (e.g., "Title", "URL").
type Country = { id: string; name: string }; const countries: Country[] = [ { id: "DK", name: "Denmark" }, { id: "SE", name: "Sweden" }, { id: "NO", name: "Norway" }, { id: "FI", name: "Finland" }, ]; const [country, setCountry] = useState<Country | undefined>(countries[0]); const onChange = (newValue: Country | undefined) => { console.log("Filter changed, calling API with new country", newValue); setCountry(newValue); }; const [filterButton, activeFilters] = useSingleFilter(country, onChange, { label: "Country", stringify: (country) => country?.name, items: countries.map((country) => ({ title: country.name, value: country })), compareFn: (a, b) => a.id === b.id, }); return <TableToolbar filter={filterButton} activeFilters={activeFilters} />;

#Basic usage with filter group

Combines multiple filters for applying several criteria simultaneously. Great for complex data sets with varying user needs. The criteria for filtering should be well-defined and relatively stable.

Use Cases:

  • Filtering an issue list by decisions, conformance, and difficulty
  • Filtering active policies by priority, category, module and access.
// these types look ambiguous, but they are just examples // of how to use filters with objects, but you can use any // type you want, including primitives type BaseFilter = { id: string; name: string }; type Country = BaseFilter; type Device = BaseFilter; type Browser = BaseFilter; type Filters = { country: Country; device?: Device; browsers?: Browser[]; }; const countries: Country[] = [ { id: "DK", name: "Denmark" }, { id: "SE", name: "Sweden" }, { id: "NO", name: "Norway" }, { id: "FI", name: "Finland" }, ]; const devices: Device[] = [ { id: "desktop", name: "Desktop" }, { id: "mobile", name: "Mobile" }, { id: "tablet", name: "Tablet" }, { id: "laptop", name: "Laptop" }, ]; const browsers: Browser[] = [ { id: "chrome", name: "Chrome" }, { id: "edge", name: "Edge" }, { id: "firefox", name: "Firefox" }, { id: "safari", name: "Safari" }, ]; const [filters, setFilters] = useState<Filters>({ country: countries[0], device: devices[1], browsers: [browsers[0], browsers[1]], }); const onChange = (newFilters: Filters) => { console.log("Filters changed, calling API with new filters", newFilters); setFilters(newFilters); }; const [filterGroup, activeFilters] = useFilterGroup<Filters>(filters, onChange, [ { label: "Country", property: "country", items: countries.map((country) => ({ title: country.name, value: country })), compareFn: (a, b) => a?.id === b?.id, stringify: ({ country }) => country?.name, }, { label: "Device", property: "device", items: devices.map((device) => ({ title: device.name, value: device })), compareFn: (a, b) => a?.id === b?.id, stringify: ({ device }) => device?.name, }, { label: "Browsers", property: "browsers", items: browsers.map((browser) => ({ title: browser.name, value: browser })), compareFn: (a, b) => a?.id === b?.id, searchable: "always", stringify: ({ browsers }) => (browsers || []).length > 0 ? browsers?.map((b) => b.name).join(", ") : undefined, }, ]); return <TableToolbar filter={filterGroup} activeFilters={activeFilters} />;

#Usage with default filter value

Setting default filter values can streamline initial interactions and guide users toward common starting points.

Use Cases:

  • Setting the default data filter to "All categories" in Core Wins
  • Setting the default sort order to "Impact: High to low" in SEO > Insight .

When using useFilterGroup or useSingleFilter, you can pass a default value to the filter by using the defaultOption property in the filter's definition. This is useful when you want to set a default value for a filter, for example when the user has not interacted with the filter yet. Also, whenever the user clears the filter, the default value will be set again.

Best Practices:

  • Choose default values that are relevant and helpful to the majority of users.
  • Clearly indicate that a filter has a default value (e.g., using static Pill).
  • Allow users to easily change the default value.

#Single filter

Country:

Denmark

type Country = { id: string; name: string }; const countries: Country[] = [ { id: "DK", name: "Denmark" }, { id: "SE", name: "Sweden" }, { id: "NO", name: "Norway" }, { id: "FI", name: "Finland" }, ]; const [country, setCountry] = useState<Country | undefined>(countries[0]); const onChange = (newValue: Country | undefined) => { console.log("Filter changed, calling API with new country", newValue); setCountry(newValue); }; const [filterButton, activeFilters] = useSingleFilter(country, onChange, { label: "Country", name: "country", defaultOption: countries[0], stringify: (country) => country?.name, items: countries.map((country) => ({ title: country.name, value: country })), compareFn: (a, b) => a.id === b.id, }); return <TableToolbar filter={filterButton} activeFilters={activeFilters} />;

#Filter group

Country:

Denmark

const countries = [ { id: "DK", name: "Denmark" }, { id: "SE", name: "Sweden" }, { id: "NO", name: "Norway" }, { id: "FI", name: "Finland" }, ]; const devices = [ { id: "desktop", name: "Desktop" }, { id: "mobile", name: "Mobile" }, { id: "tablet", name: "Tablet" }, { id: "laptop", name: "Laptop" }, ]; const [filters, setFilters] = useState({ country: countries[0], devices: [devices[0], devices[1]], }); const [filterGroup, activeFilters] = useFilterGroup(filters, setFilters, [ { label: "Country", name: "country", property: "country", defaultOption: countries[0], stringify: ({ country }) => country?.name, items: countries.map((country) => ({ title: country.name, value: country })), compareFn: (a, b) => a.id === b.id, }, { label: "Devices", name: "devices", property: "devices", stringify: ({ devices }) => (devices || []).length > 0 ? devices?.map((b) => b.name).join(", ") : undefined, items: devices.map((device) => ({ title: device.name, value: device })), compareFn: (a, b) => a.id === b.id, }, ]); return <TableToolbar filter={filterGroup} activeFilters={activeFilters} />;

#Usage with data-observe-keys

The data-observe-key attribute allows you to track user interactions with the filters. This can be valuable for analytics and understanding user behavior.

Use Cases:

  • Tracking which filters are most frequently used.
  • Analyzing how users combine different filters.
  • Identifying potential usability issues with the filtering process.

Use data-observe-key on the main component and/or individual filters to create identifiers, which are useful for tracking user interactivity, for example. Inner buttons and components, such as confirm, cancel, clear, pills, will be assigned with the same data-observe-key plus a discriminator. For instance, when using useFilterGroup(..., ..., ..., { "data-observe-key": "foo" }), the clear all button will be assigned with foo-ClearAll. See below a complete example and note this pattern in action.

Best Practices:

  • Assign unique data-observe-key values to each filter and its sub-components.
  • Use a consistent naming convention for data-observe-key values. Read more in Process for Tagging Elements (Data-observe-keys)
  • Ensure that the data-observe-key values are meaningful and easy to interpret.
const countries = [ { id: "DK", name: "Denmark", "data-observe-key": "CountryFilter-DK" }, { id: "SE", name: "Sweden", "data-observe-key": "CountryFilter-SE" }, { id: "NO", name: "Norway", "data-observe-key": "CountryFilter-NO" }, { id: "FI", name: "Finland", "data-observe-key": "CountryFilter-FI" }, ]; const devices = [ { id: "desktop", name: "Desktop", "data-observe-key": "DeviceFilter-desktop" }, { id: "mobile", name: "Mobile", "data-observe-key": "DeviceFilter-mobile" }, { id: "tablet", name: "Tablet", "data-observe-key": "DeviceFilter-tablet" }, { id: "laptop", name: "Laptop", "data-observe-key": "DeviceFilter-laptop" }, ]; const [filters, setFilters] = useState({ country: countries[0], devices: [devices[0], devices[1]], }); const [filterGroup, activeFilters] = useFilterGroup( filters, setFilters, [ { label: "Country", name: "country", property: "country", stringify: ({ country }) => country?.name, items: countries.map((country) => ({ title: country.name, value: country, "data-observe-key": country["data-observe-key"], })), compareFn: (a, b) => a.id === b.id, "data-observe-key": "CountryFilter", }, { label: "Devices", name: "devices", property: "devices", stringify: ({ devices }) => (devices || []).length > 0 ? devices?.map((b) => b.name).join(", ") : undefined, items: devices.map((device) => ({ title: device.name, value: device, "data-observe-key": device["data-observe-key"], })), compareFn: (a, b) => a.id === b.id, "data-observe-key": "DeviceFilter", }, ], { "data-observe-key": "FilterGroup-Example", } ); return <TableToolbar filter={filterGroup} activeFilters={activeFilters} />;

#Guidelines

#Best practices

#General

Use useFilterGroup and useSingleFilter when

  • Efficient data exploration is critical: Users need to quickly locate specific information within extensive datasets.
  • Customization and personalization are valued: Users have varying preferences for how they filter and refine data.
  • Multiple filtering dimensions are available: The data can be filtered across various attributes or categories.

#Placement

useFilterGroup and useSingleFilter are typically used in the following places:

  • Toolbar: The most common and visible location, typically above the table or list being filtered.
  • Side panel: Adjacent to the content, particularly useful when screen space is limited.
  • Modal: For extensive filter options, modals provide a focused environment.

#Style

  • Siteimprove Design System: Adhere to Siteimprove's guidelines for color, typography, and spacing. If you are not using a component from Fancy, match the styling of your useFilterGroup and useSingleFilter to existing components for visual consistency.
  • Clear filter option: Always provide a way to reset all filter selections (e.g., "Clear" button, "x" icon).
  • Limit options: Avoid overwhelming users with too many filter options per group (aim for 5-7).
  • Consistent placement: Maintain consistent filter placement throughout your application for predictability.

#Do not use when

  • Users can easily scan all items, filters are unnecessary.
  • One or two broad filters might be better integrated directly into the content presentation. Use Tabs instead.
  • Criteria are constantly changing or interconnected, consider a searchInput Field with Autocomplete instead.

#Accessibility

#For designers

  • Ensure sufficient color contrast, clear labels.

#For developers

This component comes with built-in accessibility, no extra work required.

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • Keep filter labels concise and clear.
  • Avoid jargon and technical terms.