Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Commerce

Filter panel

Commerce

Filter panel React component for Tailwind CSS v4

Favorite buttonListing card
Live props
Layout
mode
breakpoint
collapsible
sticky
Behavior
applyMode
hideEmpty
shortcut
Content
resultCountNo count
showChips
currency
locale
State
defaultOpen
Appearance
size
tone
color
Code
import { FilterPanel } from "@/components/ui/filter-panel"

<FilterPanel />
  1. Home
  2. Components
  3. Commerce
  4. Filter panel
Pro

Filter panel React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React filter panel for faceted search: a sidebar that applies as you go on desktop, a full-height sheet with a live count on phones.

On a wide screen the filters sit in a sidebar and every change applies at once: tap a section title to fold or unfold it (a dot marks the ones in use, and a folded section shows its choice under the title, like “Apple, Samsung”), drag the price thumbs over the histogram and let go, type a price and press Enter, pick a category row, tick a brand, tap a condition or radius chip, flip a switch. Tap “Show 7 more” for the rest of a long list, or type in its search field when it has more than ten options. The chips under the title remove one value each with their × (or Backspace and Delete while focused), and “Clear all” in the header clears every filter. With applyMode set to apply the sidebar keeps a draft until you press Apply. Below the breakpoint a Filters pill with the active count takes the sidebar’s place: tap it and a full-height sheet slides up (a side panel on desktop in sheet mode) with the same sections, the first two open, and a footer button that counts your draft as you choose (“Show 1.240 results”, a spinner while it counts, “No results, try fewer filters” when nothing matches). The button applies and closes; swiping the sheet down, tapping the backdrop or pressing Escape throws the draft away, and Reset clears only the draft. The place field opens its own searchable sheet on a phone, and “My location” fills it in when the page can locate you. Keyboard: Tab walks the sections, Enter or Space folds them and arrows move between their titles; every control keeps its own keys (arrows and Page keys on the sliders, Enter in the inputs, arrows in the chip and radio groups). A shortcut prop (like mod+shift+f) opens the sheet, or jumps to the first section while the sidebar shows. Under reduced motion sections, chips and the count fade instead of moving.

Filter panel is a Pro React component in the React commerce components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add filter-panel, together with Accordion, Badge, Button, Category picker, Checkbox and 22 more and the npm packages lucide-react and motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add filter-panel

Filter panel is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import FilterPanel from @/components/ui/filter-panel.

import { useState } from "react"
import { Truck } from "lucide-react"
import { FilterPanel, type FilterDef, type FilterValues } from "@/components/ui/filter-panel"

const filters: FilterDef[] = [
  { type: "price", id: "price", label: "Price", min: 0, max: 1800, presets: [{ min: null, max: 100 }, { min: 100, max: 300 }] },
  {
    type: "chips",
    id: "condition",
    label: "Condition",
    options: [
      { value: "new", label: "New", count: 310 },
      { value: "used", label: "Used", count: 843 },
      { value: "parts", label: "For parts", count: 87 },
    ],
  },
  {
    type: "checkbox",
    id: "brand",
    label: "Brand",
    options: [
      { value: "apple", label: "Apple", count: 312 },
      { value: "samsung", label: "Samsung", count: 287 },
      { value: "xiaomi", label: "Xiaomi", count: 164 },
    ],
  },
  { type: "toggle", id: "delivery", label: "Delivery only", icon: <Truck /> },
]

export function PhoneFilters({ count }: { count: (values: FilterValues) => Promise<number> }) {
  const [values, setValues] = useState<FilterValues>({})
  return (
    <div className="flex flex-col gap-4 lg:flex-row lg:items-start">
      <FilterPanel
        filters={filters}
        value={values}
        onValueChange={setValues}
        getResultCount={count}
        locale="en-US"
        className="lg:w-72 lg:shrink-0"
      />
      <main className="min-w-0 flex-1">{/* results */}</main>
    </div>
  )
}

Props

  • filters*
    FilterDef[]

    The sections, in order. A union on type: category (a CategoryPicker list), price (a PriceRange over a histogram), location (a place combobox, popular places and radius chips; "My location" with onLocate), checkbox (rows with counts, a limit and "Show more", a search field past ten options), radio (with an "any" row), chips (single or multiple), range (a two-thumb slider and two number inputs with a unit), toggle (a switch row) and custom (render, describe, isActive). Every filter has an id, a label, an optional description and defaultCollapsed. Unknown types are skipped with a warning in development.

  • value
    FilterValues

    The committed filters, keyed by filter id: category and radio a string, price and range [min, max] with null for an open end, location { place, radius }, checkbox and chips string[], toggle true. Inactive filters leave the object; keys of filters not in the list are kept.

  • defaultValue
    FilterValues

    The starting filters while uncontrolled. Defaults to {}.

  • onValueChange
    (values: FilterValues) => void

    The sidebar in instant mode calls it on every commit (a slider release, a toggle, a pick, never per keystroke); the sheet and apply mode call it on the Apply button; chips and Clear all call it at once in the sidebar.

  • modedefault "auto"
    "auto" | "sidebar" | "sheet"

    auto shows the sidebar from the breakpoint up and the Filters button with a bottom sheet below it (switched with CSS, so the server HTML is right). sidebar always shows the sidebar; sheet always shows the button, with a side panel on desktop.

  • breakpointdefault "lg"
    "md" | "lg"

    Where auto switches from the sheet to the sidebar: 768px or 1024px. Crossing it with the sheet open closes the sheet.

  • applyModedefault "instant"
    "instant" | "apply"

    The sidebar's behavior: instant commits each change, apply keeps a draft until the Apply button in its footer. The sheet always keeps a draft.

  • getResultCount
    (values: FilterValues) => (number | Promise<number>)

    Counts the sheet's draft for its button, 250ms after the last change (the first count of an opening runs at once). A promise shows a spinner; an older answer never overwrites a newer one; a rejection falls back to the plain Show results label.

  • resultCountdefault 0
    number

    A fixed count for the sheet's button when there is no getResultCount. 0 reads Show results.

  • open
    boolean

    The sheet's open state, controlled. Opening copies the committed value into a draft; a value changing while it is open does not touch the draft.

  • defaultOpendefault false
    boolean

    Starts with the sheet open (only where the sheet is in use: sheet mode, or auto below the breakpoint).

  • onOpenChange
    (open: boolean) => void

    Called when the sheet opens or closes, by the button, a shortcut, a swipe, the backdrop or Escape.

  • triggerdefault "<FilterPanelTrigger />"
    ReactNode

    Replaces the Filters button that opens the sheet (a single element that takes a ref). null renders none, so you can open the sheet from a toolbar with open and onOpenChange.

  • showChipsdefault true
    boolean

    The active filters as removable chips under the title (sidebar) or at the top of the sheet.

  • collapsibledefault true
    boolean

    Sections fold like an accordion. false keeps every section open under a plain heading.

  • defaultExpanded
    string[]

    Filter ids open at first. Defaults to the first four sections in the sidebar and the first two in the sheet (skipping toggles and those with defaultCollapsed).

  • hideEmptydefault false
    boolean

    Hides options with a count of 0 instead of showing them disabled. A picked option always stays.

  • stickydefault true
    boolean

    The sidebar sticks under var(--topbar-offset, 0px) and scrolls on its own when it is taller than the screen.

  • sizedefault "md"
    "sm" | "md"

    The controls inside: checkboxes, switches, sliders, fields and chips. sm suits a narrow sidebar.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    Checked boxes, picked chips, the slider ranges, the lit histogram bars, the section dots, the count badge and the Apply button.

  • colordefault ""
    string

    Any CSS color; replaces the tone everywhere it paints.

  • shortcutdefault ""
    string

    A key combo in mod+k notation that opens (and closes) the sheet, or jumps to the first section while the sidebar shows. The button shows it as a key hint on devices with a keyboard. None by default.

  • currencydefault "USD"
    string

    ISO 4217 code for the price section, its chips and summaries.

  • localedefault ""
    string

    Counts, prices and ranges (1,240 in en-US). Empty uses the UIProvider's locale, then the browser's.

  • container
    HTMLElement | null

    Portal target of the sheet. Defaults to document.body.

  • labels
    Partial<FilterPanelLabels>

    Every string, English by default (DEFAULT_FILTER_PANEL_LABELS): title "Filters", clearAll "Clear all", reset "Reset", apply "Apply", showResults (count, formatted), showResultsUnknown "Show results", noResults "No results, try fewer filters", any "Any", anywhere "Anywhere", radius (km) "+25 km" ("Only here" for 0), distance, myLocation "My location", showMore (count), showLess, searchOptions, noMatches, from, to, rangeFrom (value), rangeTo (value), trigger "Filters", active (count) "3 active", chips, remove, clear, cancel (the place field and its phone sheet), plus priceRange and categoryPicker labels passed through.

  • classNames
    Partial<Record<"sidebar" | "header" | "chips" | "section" | "trigger" | "content" | "footer" | "apply", string>>

    Extra classes for the parts: the sidebar's aside (give it a panel look here), the headers, the chip row, each section, the Filters button, the sheet, the footers and the Apply buttons.

  • className
    string

    Extra classes for the root; the page sets the sidebar's width here (lg:w-72).

Parts

  • FilterPanelTrigger

    component

    The Filters pill on its own: an outline button with the sliders icon and a count badge that pops when it changes; its accessible name reads "Filters, 3 active" and a shortcut goes to aria-keyshortcuts (its key hint is hidden from screen readers and on touch). tone and color paint the badge. Props: ButtonProps & { count?: number; label?: ReactNode; activeLabel?: (count: number) => string; shortcut?: string; locale?: string }.

  • FilterChips

    component

    The active filters as removable chips for a toolbar or over the results, with Clear all once there are two. Renders nothing while no filter is active. Props: { filters; value?; defaultValue?; onValueChange?; showClearAll?; size?; tone?; color?; currency?; locale?; labels?; classNames? }.

  • describeFilters

    function

    The chips as data, also the shape of toolbar's appliedFilters: one per checkbox or chip value ("Apple"), one per other filter ("Price: $500 to $3,000", "Chicago +25 km"). Signature: (filters: FilterDef[], values: FilterValues, options?: { locale?; currency?; labels? }) => { id: string; filterId: string; label: ReactNode }[].

  • removeFilterValue

    function

    The values without one chip: a single checkbox value, or the whole filter. Signature: (filters: FilterDef[], values: FilterValues, chipId: string) => FilterValues.

  • countActiveFilters

    function

    How many filters are in use (two brands count once), for a badge or a heading. Signature: (filters: FilterDef[], values: FilterValues) => number.

  • FilterDef

    type

    One filter, by type: category, price, location, checkbox, radio, chips, range, toggle or custom, each with an id and a label. The option lists take FilterOption: { value, label, count?, icon?, description?, disabled?, keywords?, textValue? }; a count of 0 disables the option.

  • DEFAULT_FILTER_PANEL_LABELS

    constant

    Every English string: title ("Filters"), Clear all, Reset, Apply, showResults ("Show 1,284 results"), the Any / Anywhere options, distance and radius, show more and less, the range captions, the trigger and chip row labels. Spread it into labels to translate a few. Type: FilterPanelLabels.

  • FilterValues

    type

    The panel's value: a record keyed by filter id. Prices and ranges hold FilterRangeValue ([min, max], null for an open end), locations hold FilterLocationValue ({ place, radius }).

Examples

Sheet on a phone

mode="sheet" with a getResultCount that takes 400ms: the button spins, then fades to the new count.

Sidebar with an Apply button

applyMode="apply" keeps a draft until Apply filters; chips still remove at once.

Chips under a toolbar

A toolbar holds the sheet's Filters button and the sort; FilterChips below it: one chip per value, Clear all once there are two.

Real estate

A log price track, bedroom chips, area and year ranges, a floor radio and an owner toggle.

Cars, size sm

Forty brands behind a search field, year and mileage ranges, fuel chips.

Zero results

The sheet's button reads No matches and stays pressable.

Dependencies

lucide-reactmotionaccordionbadgebuttoncategory-pickercheckboxcomboboxcommercedrawerinputkbdmotionnumber-formatnumber-inputprice-rangeradio-groupsliderspinnerswitchtagtext-matchtonesui-configuse-controllable-stateuse-debounceuse-localeuse-media-queryuse-shortcut

Files

  • components/ui/filter-panel.tsx

FAQ

Is the React Filter panel component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Filter panel?

Run npx wingo-ui@latest add filter-panel. It also adds the Wingo UI items it builds on (Accordion, Badge, Button, Category picker, Checkbox and 22 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/filter-panel.

How does Filter panel behave on phones?

The place field opens its own searchable sheet on a phone, and “My location” fills it in when the page can locate you.

See also

ToolbarPrice rangeCategory pickerDrawerAccordion

Used in

Marketplace Search Page

More React commerce components

Listing cardProduct galleryFavorite buttonOrder trackerCheckout stepsCart drawer
All 16 React commerce components

Articles using this component

  • React Filter Builder: AND / OR Chips and Saved Views

    Component guides · Nov 6, 2026 · 11 min read

  • React Marketplace Template That Works Like an App

    Build it: screens and apps · Oct 16, 2026 · 15 min read

PreviousFavorite buttonNext Listing card

Filters

Cell phones

Phones

1,728 listings
  1. ›

$MinAmount in US dollars
–
$MaxAmount in US dollars

“phone”

1,240 listings
Cell phones

Apple iPhone 15 Pro, 64 GB

$710, Negotiable

Nashville·, Today at 4:16 PM

DeliveryUsed
Apple iPhone 15 Pro, 64 GB, photo 1 of 1

Apple iPhone 12, 128 GB

$780, Negotiable

Seattle·, Today at 4:16 PM

DeliveryNew
Apple iPhone 12, 128 GB, photo 1 of 1

Samsung Galaxy Z Flip5, 256 GB

$590, Negotiable

Fort Worth·, Today at 4:16 PM

DeliveryUsed
Samsung Galaxy Z Flip5, 256 GB, photo 1 of 1

1 active · open false · {"category":"cell-phones"}