Filter Search Panel Block
FilterSearchPanel is a search-and-filter toolbar built on MUI with a submit search field, filter settings button, and removable filter chips.
Installationโ
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-filter-search-panel-block
yarn add @nodeblocks/frontend-filter-search-panel-block
pnpm add @nodeblocks/frontend-filter-search-panel-block
bun add @nodeblocks/frontend-filter-search-panel-block
What You Needโ
| Item | Why it matters |
|---|---|
filters | Active filter chips shown in the panel ({ label, key, groupName? }) |
defaultSearchValue | Initial value for the search field |
onSearch | Called on form submit with parsed form data (default field: search) |
onSearchChange | Receives search input change events when you want controlled search |
onClickRemoveFilter | Removes a chip when the user deletes it |
onClickFilterButton | Opens your filter UI (modal, drawer, route, etc.) |
searchPlaceholder | Placeholder text used by SearchInput |
noFilterText | Text shown when no filters are selected |
filterLabel | Label shown on the filter settings button |
FilterSearchPanel does not own the filters array. Keep chips in your app state and update them in onClickRemoveFilter (and when applying filters elsewhere).
Code Examplesโ
- Quick Start
- Labels
- Compound Components
- Block Override
function Example() { const defaultFilters = [ {label: 'Active', key: 'status-active'}, {label: 'Inactive', key: 'status-inactive'}, ]; const filterCatalog = [ {label: 'Active', key: 'status-active'}, {label: 'Inactive', key: 'status-inactive'}, {label: 'Remote', key: 'work-remote', groupName: 'Work type'}, {label: 'Full-time', key: 'employment-full-time', groupName: 'Employment'}, {label: 'Contract', key: 'employment-contract', groupName: 'Employment'}, ]; const [filters, setFilters] = React.useState(defaultFilters); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = ({search}) => { if (!search?.trim()) { return; } setFilters(current => [...current, {label: search, key: `search-${Date.now()}`, groupName: 'Search'}]); }; return ( <FilterSearchPanel filters={filters} searchPlaceholder="Search services..." noFilterText="No filters applied" filterLabel="Filter Settings" onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} /> ); }
function Example() { const filterCatalog = [ {label: 'Open', key: 'status-open', groupName: 'Status'}, {label: 'In progress', key: 'status-in-progress', groupName: 'Status'}, {label: 'Archived', key: 'status-archived', groupName: 'Status'}, ]; const [filters, setFilters] = React.useState([]); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = data => { if (!data.search?.trim()) { return; } setFilters(current => [...current, {label: data.search, key: `search-${Date.now()}`}]); }; return ( <FilterSearchPanel filters={filters} defaultSearchValue="React development" searchPlaceholder="Type to find projects..." noFilterText="All projects shown" filterLabel="Advanced Options" onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} /> ); }
Use child blocks to customize layout and styling.
function Example() { const handleSearch = ({search}) => { //your debounced search }; return ( <FilterSearchPanel searchPlaceholder="Search services..." onSearch={handleSearch}> <FilterSearchPanel.SearchInput /> </FilterSearchPanel> ); }
Use function children to override default blocks, change order, or inject custom blocks.
function Example() { const defaultFilters = [ {label: 'Active', key: 'status-active'}, {label: 'Web Development', key: 'category-web'}, ]; const filterCatalog = [ {label: 'Active', key: 'status-active'}, {label: 'Web Development', key: 'category-web'}, {label: 'Premium', key: 'tier-premium', groupName: 'Tier'}, {label: 'Enterprise', key: 'tier-enterprise', groupName: 'Tier'}, ]; const [filters, setFilters] = React.useState(defaultFilters); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = ({search}) => { if (!search?.trim()) { return; } setFilters(current => [...current, {label: search, key: `search-${Date.now()}`}]); }; return ( <FilterSearchPanel filters={filters} filterLabel="Options" searchPlaceholder="Search services..." onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, searchTip: ( <Box sx={{ px: 1.5, py: 1, bgcolor: 'info.light', borderRadius: 1, fontSize: 'body2.fontSize', color: 'info.dark', }} > Press Enter to search or use filters to narrow results. </Box> ), }, blockOrder: [...defaultBlockOrder, 'searchTip'], })} </FilterSearchPanel> ); }
When to use block overrides
Use overrides when you need to change block order, replace a default block, or add custom block (like searchTip above) while keeping search and chip behavior on the root component.
Important Propsโ
Core Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
filters | FilterChip[] | No | [] | Selected filter chips shown in the panel |
defaultSearchValue | string | No | undefined | Initial value used by SearchInput |
onSearch | (data: T) => void | No | undefined | Called on form submit; T defaults to { search: string } |
onSearchChange | (event: React.ChangeEvent<HTMLInputElement>) => void | No | undefined | Search field change handler, typically used for controlled search input |
onClickRemoveFilter | (filter: FilterChip) => void | No | undefined | Called when a chip delete icon is clicked |
onClickFilterButton | () => void | No | undefined | Called when the filter settings button is clicked |
Content Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
searchPlaceholder | string | No | ใใชใผใฏใผใใงๆค็ดข | Placeholder shown in the search input |
noFilterText | string | No | ๆกไปถๆช่จญๅฎ | Text shown when no filters are selected |
filterLabel | string | No | ็ต่พผใฟ่จญๅฎ | Label shown on the filter settings button |
Layout and Composition Propsโ
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | No | undefined | Override default blocks or render compound children |
className | string | No | undefined | Class name on the root form |
sx | SxProps | No | undefined | MUI system styles for the root form |
spacing | StackProps['spacing'] | No | { xs: 1.5, sm: 2.5 } | Space between root blocks |
direction | StackProps['direction'] | No | 'column' | Flex direction of the root stack |
FilterSearchPanel inherits StackProps (except children, component, and onSubmit) and renders as a form. Submit is handled internally and calls onSearch when provided.
Sub-component propsโ
Sub-components share context with root so by default they use prop values from context, but they can be overriden locally:
| Sub-component | Main props | Inherits |
|---|---|---|
FilterSearchPanel.SearchInput | searchPlaceholder, defaultSearchValue, onSearchChange, name, type | TextField |
FilterSearchPanel.FilterButton | filterLabel, onClickFilterButton, children | Button |
FilterSearchPanel.SelectedFilterList | filters, noFilterText, onClickRemoveFilter, children | Box |
FilterSearchPanel.FilterBadge | filter (required), onClickRemoveFilter, children | Chip |
FilterSearchPanel.ActionGroup | children (default: filter button + chip list) | Stack |
Default UI Blocksโ
| Block | Built on | Notes |
|---|---|---|
FilterSearchPanel (root) | Stack rendered as form | Handles submit internally and provides search/filter context to child blocks |
FilterSearchPanel.SearchInput | TextField + submit IconButton | name="search", size="small", search icon submits the form |
FilterSearchPanel.ActionGroup | Stack | Horizontal row: filter button + selected chips (scrollable) |
Default render order: searchInput โ actionGroup.
Extra field primitivesโ
| Primitive | Built on | Notes |
|---|---|---|
FilterSearchPanel.FilterButton | Button + Tune icon | Opens your filter UI via onClickFilterButton; default label is ็ต่พผใฟ่จญๅฎ |
FilterSearchPanel.SelectedFilterList | Box + Typography | Shows noFilterText (ๆกไปถๆช่จญๅฎ) when empty, otherwise groups chips by groupName |
FilterSearchPanel.FilterBadge | Chip | Removable chip; delete calls onClickRemoveFilter(filter) |
TypeScriptโ
import * as React from 'react';
import {FilterSearchPanel} from '@nodeblocks/frontend-filter-search-panel-block';
type FilterChip = {
label: string;
key: string;
groupName?: string;
};
type SearchFormData = {
search: string;
};
const filterCatalog: FilterChip[] = [
{label: 'Remote', key: 'work-remote', groupName: 'Work type'},
{label: 'Full-time', key: 'employment-full-time', groupName: 'Employment'},
];
export function ServiceSearchToolbar() {
const [filters, setFilters] = React.useState<FilterChip[]>([
{label: 'Active', key: 'status-active', groupName: 'Status'},
]);
const handleRemoveFilter = (filter: FilterChip) => {
setFilters(current => current.filter(f => f.key !== filter.key));
};
const handleFilterButton = () => {
const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key));
if (nextFilter) {
setFilters(current => [...current, nextFilter]);
}
};
const handleSearch = (data: SearchFormData) => {
const search = data.search.trim();
if (!search) {
return;
}
setFilters(current => [...current, {label: search, key: `search-${Date.now()}`, groupName: 'Search'}]);
};
return (
<FilterSearchPanel<SearchFormData>
filters={filters}
searchPlaceholder="Search services..."
noFilterText="No filters applied"
filterLabel="Filter Settings"
onClickFilterButton={handleFilterButton}
onClickRemoveFilter={handleRemoveFilter}
onSearch={handleSearch}
/>
);
}