@clywell/filter-toolbar
v1.1.0
Published
A flexible and powerful React filter system with framework adapters for Next.js, React Router, and more
Downloads
31
Maintainers
Readme
@clywell/filter-toolbar
A flexible and powerful React filter system with framework adapters for Next.js, React Router, and more. Build dynamic, persistent filter interfaces with minimal configuration.
Features
- 🚀 Framework Agnostic - Works with Next.js, React Router, or any routing solution
- 💾 Persistent Filters - URL params, localStorage, or custom persistence adapters
- 🎨 Customizable UI - Bring your own components or use the defaults
- 📱 Mobile Responsive - Automatic mobile/desktop layout switching
- 🔍 Rich Filter Types - Text, select, date ranges, number ranges, lookups, and more
- 🎯 TypeScript First - Full type safety and IntelliSense support
- 🌳 Tree Shakeable - Import only what you need
Installation
npm install @clywell/filter-toolbar
# or
yarn add @clywell/filter-toolbar
# or
pnpm add @clywell/filter-toolbarQuick Start
Basic Usage (Next.js)
import React from 'react';
import {
useFilterBuilder,
FilterToolbar,
createNextJSAdapter,
type FilterDefinition
} from '@clywell/filter-toolbar';
// Optional: Import default styles
import '@clywell/filter-toolbar/dist/styles.css';
const availableFilters: FilterDefinition[] = [
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' }
]
},
{
key: 'name',
label: 'Name',
type: 'text'
},
{
key: 'dateRange',
label: 'Date Range',
type: 'date-range'
}
];
export function MyComponent() {
const filterBuilder = useFilterBuilder({
availableFilters,
persistenceAdapter: createNextJSAdapter(),
onQueryChange: (query) => {
console.log('Filter query changed:', query);
// Use query to fetch filtered data
}
});
return (
<div>
<FilterToolbar
availableFilters={availableFilters}
activeFilters={filterBuilder.activeFilters}
onAddFilter={filterBuilder.addFilter}
onUpdateFilter={filterBuilder.updateFilter}
onRemoveFilter={filterBuilder.removeFilter}
onClearAll={filterBuilder.clearFilters}
hasActiveFilters={filterBuilder.hasActiveFilters}
/>
{/* Your filtered content here */}
<div>
<pre>{JSON.stringify(filterBuilder.query, null, 2)}</pre>
</div>
</div>
);
}React Router Usage
import { useNavigate, useLocation } from 'react-router-dom';
import { createReactRouterAdapter } from '@clywell/filter-toolbar';
export function MyComponent() {
const navigate = useNavigate();
const location = useLocation();
const persistenceAdapter = createReactRouterAdapter(navigate, location);
const filterBuilder = useFilterBuilder({
availableFilters,
persistenceAdapter,
onQueryChange: (query) => {
// Handle query changes
}
});
// ... rest of component
}Memory/LocalStorage Usage
import { createLocalStorageAdapter, createMemoryAdapter } from '@clywell/filter-toolbar';
// For localStorage persistence
const persistenceAdapter = createLocalStorageAdapter('my-filters');
// For in-memory only (useful for testing)
const persistenceAdapter = createMemoryAdapter();
const filterBuilder = useFilterBuilder({
availableFilters,
persistenceAdapter,
onQueryChange: (query) => {
// Handle query changes
}
});Styling
The package is completely styling-agnostic - no CSS framework dependencies!
Option 1: Use Default Styles
// Import the default CSS variables-based styles
import '@clywell/filter-toolbar/dist/styles.css';Option 2: Customize with CSS Variables
:root {
/* Customize any aspect */
--filter-primary: #your-brand-color;
--filter-spacing-sm: 0.75rem;
--filter-radius-md: 0.75rem;
}Option 3: Complete Custom Styling
/* Target semantic class names */
.filter-button { /* your styles */ }
.filter-dropdown__content { /* your styles */ }
.filter-chip { /* your styles */ }Option 4: Component Override
<FilterToolbar
components={{
Button: MyCustomButton,
Badge: MyCustomBadge,
// Override any component
}}
// ... other props
/>See STYLING.md for complete customization guide.
Filter Types
Text Filter
{
key: 'search',
label: 'Search',
type: 'text'
}Select Filter
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' }
]
}Multi-Select Filter
{
key: 'categories',
label: 'Categories',
type: 'multi-select',
options: [
{ value: 'tech', label: 'Technology' },
{ value: 'design', label: 'Design' },
{ value: 'marketing', label: 'Marketing' }
]
}Date Filter
{
key: 'createdDate',
label: 'Created Date',
type: 'date'
}Date Range Filter
{
key: 'dateRange',
label: 'Date Range',
type: 'date-range'
}Number Filter
{
key: 'price',
label: 'Price',
type: 'number'
}Number Range Filter
{
key: 'priceRange',
label: 'Price Range',
type: 'number-range'
}Boolean Filter
{
key: 'isActive',
label: 'Is Active',
type: 'boolean'
}Custom Lookup Filter
{
key: 'userId',
label: 'User',
type: 'lookup',
lookupKey: 'users' // Your custom lookup key
}Custom Components
You can override any UI component by passing them through the components prop:
import { Button, Sheet, SheetContent } from '@/components/ui';
<FilterToolbar
// ... other props
components={{
Button: Button,
Sheet: Sheet,
SheetContent: SheetContent,
// ... other component overrides
}}
/>Available Component Overrides
Button- All buttons in the interfaceSheet,SheetContent,SheetHeader,SheetTitle,SheetTrigger- Mobile sheet componentsInput- Text inputsSelect,SelectContent,SelectItem,SelectTrigger,SelectValue- Select dropdownsBadge- Filter chipsPopover,PopoverContent,PopoverTrigger- Popup overlaysCalendar- Date pickersSwitch- Boolean togglesDropdownMenu,DropdownMenuContent,DropdownMenuItem,DropdownMenuTrigger- Dropdown menus
Custom Lookup Function
Provide your own lookup function for dynamic data:
const lookupFunction = async (key: string) => {
switch (key) {
case 'users':
const users = await fetchUsers();
return users.map(user => ({
value: user.id,
label: user.name
}));
case 'categories':
const categories = await fetchCategories();
return categories.map(cat => ({
value: cat.id,
label: cat.name
}));
default:
return [];
}
};
const filterBuilder = useFilterBuilder({
availableFilters,
lookupFunction,
persistenceAdapter,
onQueryChange: (query) => {
// Handle query changes
}
});Creating Custom Persistence Adapters
You can create your own persistence adapter by implementing the PersistenceAdapter interface:
import type { PersistenceAdapter } from '@clywell/filter-toolbar';
const customAdapter: PersistenceAdapter = {
saveFilters: (filters) => {
// Save filters to your preferred storage
localStorage.setItem('filters', JSON.stringify(filters));
},
loadFilters: (availableFilters) => {
// Load filters from your storage
const saved = localStorage.getItem('filters');
if (saved) {
return JSON.parse(saved);
}
return [];
},
clearFilters: () => {
// Clear filters from your storage
localStorage.removeItem('filters');
}
};API Reference
useFilterBuilder(options)
Main hook for managing filter state.
Options
availableFilters: FilterDefinition[]- Array of available filter definitionsonQueryChange?: (query: FilterQuery) => void- Callback when filter query changesinitialFilters?: ActiveFilter[]- Initial filters to setpersistenceAdapter?: PersistenceAdapter- Adapter for filter persistencelookupFunction?: LookupFunction- Function for resolving lookup data
Returns
activeFilters: ActiveFilter[]- Currently active filtersisBuilderOpen: boolean- Whether the filter builder is opensetIsBuilderOpen: (open: boolean) => void- Toggle filter builderaddFilter: (definition: FilterDefinition) => void- Add a new filterupdateFilter: (filterId: string, value: unknown) => void- Update filter valueremoveFilter: (filterId: string) => void- Remove a filterclearFilters: () => void- Clear all filtershasActiveFilters: boolean- Whether there are active filtersquery: FilterQuery- Current filter query object
FilterToolbar
Main filter toolbar component.
Props
availableFilters: FilterDefinition[]- Available filtersactiveFilters: ActiveFilter[]- Active filtersonAddFilter: (definition: FilterDefinition) => void- Add filter handleronUpdateFilter: (filterId: string, value: unknown) => void- Update filter handleronRemoveFilter: (filterId: string) => void- Remove filter handleronClearAll: () => void- Clear all filters handlerhasActiveFilters: boolean- Whether there are active filtersclassName?: string- Additional CSS classesisMobile?: boolean- Force mobile/desktop modecomponents?: ComponentOverrides- Custom component overrides
Examples
Check out the examples/ directory for complete working examples:
examples/nextjs/- Next.js App Router exampleexamples/react-router/- React Router exampleexamples/basic/- Basic React example with localStorage
Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
License
MIT License - see LICENSE file for details.
