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

Category picker

Commerce

Category picker React component for Tailwind CSS v4

Cart drawerCheckout steps
Live props
State
defaultValue
defaultQuery
Appearance
variant
iconStyle
size
tone
color
Layout
columnsAuto
Behavior
selectableParents
search
locale
Content
showCounts
Code
import { CategoryPicker } from "@/components/ui/category-picker"

<CategoryPicker />
  1. Home
  2. Components
  3. Commerce
  4. Category picker
Pro

Category picker React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React category picker: browse a category tree with icon tiles, drill-down rows with a back button, counts and search across levels.

Tap a tile or a row with subcategories and its children slide in from the right; the back button (or the breadcrumb under the title, which folds its middle four levels deep) slides you back out. Tap a leaf to pick it: it tints and a check draws in, and parents on the way to your choice carry a small dot. With selectableParents the first row of a level, “All in Electronics”, picks the whole branch. Type in the search to find a category at any depth: matches are highlighted, exact words and leaves come first (then the busier category), each row shows its path, and clearing the search (its X, Escape, the back button, or the Clear search button under “No categories found”) returns to the level you were on. The field shows the picked path in a form box and opens the picker in a popover on desktop and in a full-height bottom sheet on phones, where the sheet's title row doubles as the picker's header (the level's name and the back button appear in it as you drill in); picking a leaf closes it and returns focus to the field. Keyboard: Tab reaches the back button, the search and one item per level; in the grid the arrows move in two dimensions, in lists Up and Down move, Right opens a parent and Left goes back; Home, End, Page Up and Page Down jump; Enter or Space opens or picks; Backspace outside the search goes up; Escape clears the search, then goes up a level, and at the top closes the field's overlay. In the search, Down moves into the list and Enter picks the first match. After a drill focus lands on the first item, after going back on the parent you came from. On phones tiles sit three to a row, rows are 56px and the back button is 44px; the search only takes focus by itself on a mouse or trackpad so the keyboard never covers the tree. Under reduced motion levels cross-fade instead of sliding.

Category picker 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 category-picker, together with Button, Field, Input, Popover, useControllableState and 8 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 category-picker

Category picker 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 CategoryPicker from @/components/ui/category-picker.

import { Smartphone, House } from "lucide-react"
import { CategoryPicker, CategoryPickerField, type CategoryNode } from "@/components/ui/category-picker"

const categories: CategoryNode[] = [
  {
    id: "electronics",
    label: "Electronics and appliances",
    icon: <Smartphone />,
    color: "#7C3AED",
    count: 128430,
    children: [
      { id: "mobile-phones", label: "Mobile phones", count: 38420, keywords: ["iphone"] },
      { id: "computers", label: "Laptops and computers", count: 18940 },
    ],
  },
  { id: "real-estate", label: "Real estate", icon: <House />, color: "#16A34A", count: 98215 },
]

export function PickCategory() {
  return (
    <div className="flex flex-col gap-6">
      <CategoryPicker
        categories={categories}
        onValueChange={(id, path) => console.log(id, path.map((n) => n.label).join(" › "))}
        labels={{ search: "Find a category", allCategories: "Everything" }}
      />
      <CategoryPickerField
        categories={categories}
        label="Category"
        placeholder="Pick a category"
        required
        name="category"
      />
    </div>
  )
}

Props

  • categories*
    CategoryNode[]

    The tree. CategoryNode = { id; label; icon?; image?; color?; count?; description?; keywords?; href?; disabled?; children? }. A node with children drills, one without (or with children: []) is picked. color tints the icon tile and is inherited by the children; keywords feed the search; href turns a leaf into a link (browse mode).

  • value
    string

    The picked category id, controlled. A value that is not in the tree shows nothing selected and never crashes.

  • defaultValuedefault ""
    string

    The starting value while uncontrolled. The picker opens on the level that holds it, so a reopened field shows the current choice in context.

  • onValueChange
    (id: string, path: CategoryNode[]) => void

    Fires when another category is picked, with the path from the top level down to it.

  • onPick
    (id: string, path: CategoryNode[]) => void

    Fires on every pick, including the current value again. Overlays close on it (the field does).

  • variantdefault "grid"
    "grid" | "list"

    grid: icon tiles at the top level and rows below it (a marketplace home). list: rows everywhere, for sheets, sidebars and filter panels.

  • columnsdefault 0
    number

    Tile columns. 0 fits the container: 3 in a phone-width box, 4 from 448px, 6 from 672px (container widths, so it also works in a sidebar or a popover).

  • selectableParentsdefault false
    boolean

    A parent can be picked through an “All in …” row at the top of its level (search a whole branch). Otherwise parents only drill.

  • searchdefault true
    boolean

    The search box over every level. Enter picks the first match; it never submits a surrounding form.

  • showCountsdefault true
    boolean

    Listing counts on tiles, rows and the level header, formatted for the locale (128.430). Screen readers hear them with labels.count.

  • iconStyledefault "tile"
    "tile" | "plain"

    tile puts the icon in a rounded square tinted with the node's color (neutral gray without one); plain shows the colored icon alone.

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

    Tiles 88 / 104 / 120px tall with 40 / 48 / 56px icon tiles; rows 44 / 56 / 64px; the search box follows.

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

    The selected tile or row: a gray tint for neutral, the soft tone otherwise, plus the check and the dot on parents that hold the choice.

  • colordefault ""
    string

    Any CSS color for the selected look; replaces the tone. Category colors come from the nodes, not from this.

  • localedefault ""
    string

    Formats the counts and the results line. Empty uses the UIProvider's locale, then the browser's.

  • query
    string

    The search text, controlled. Pair it with onQueryChange.

  • defaultQuerydefault ""
    string

    The search text on the first render, e.g. a term carried over from the site search.

  • onQueryChange
    (query: string) => void

    Fires as the search text changes.

  • linkComponent
    (props: LinkComponentProps) => ReactNode

    Renders leaves that have an href (browse mode) instead of picking them. Defaults to the LinkProvider's component (Next's Link in an app), then a plain <a>.

  • labels
    Partial<CategoryPickerLabels>

    Every string, English by default (DEFAULT_CATEGORY_PICKER_LABELS): search "Search categories", back "Back", allCategories "All categories", all (label) => "All in …", empty "No categories found", results (n, formatted) => "12 categories", count (n, formatted) => "128,430 listings", separator "›", subcategories "has subcategories", containsSelected "contains your choice", clear "Clear search", breadcrumb "Category path".

  • classNames
    Partial<Record<"top" | "header" | "back" | "title" | "breadcrumb" | "search" | "panel" | "grid" | "list" | "item" | "icon" | "label" | "count" | "results" | "empty", string>>

    Extra classes for the inner parts. top wraps the header, breadcrumb and search (the field pins it with sticky).

Parts

  • CategoryPickerField

    component

    The form field: the W2 field anatomy (label, description, error, required, size, variant soft | outline, radius, name for a hidden input) around a box that shows the picked path with the leaf's icon, placeholder "Choose a category". Opens a CategoryPicker variant="list" in a popover on desktop and a full-height bottom sheet on phones (responsive, default true; sheetTitle defaults to the label's text, then "Category", and heads the top level inside the sheet). open / defaultOpen / onOpenChange, container, plus the picker's selectableParents, search, showCounts, iconStyle, tone, color, locale, labels and linkComponent; classNames (field parts, trigger, value, chevron, content), pickerClassNames and fieldProps reach the inner parts. Picking a leaf closes it and returns focus to the field; a form reset restores defaultValue. Props: CategoryPickerFieldProps.

  • findCategoryPath

    function

    The path from the top level down to the node with this id (the node last), or [] when it is not in the tree. Breadcrumbs, readouts and filters use it. Signature: (categories: CategoryNode[], id: string) => CategoryNode[].

  • flattenCategories

    function

    Every node in reading order with its path; the search runs over it, and a plain select of categories can too. Signature: (categories: CategoryNode[]) => { node: CategoryNode; path: CategoryNode[] }[].

  • DEFAULT_CATEGORY_PICKER_LABELS

    constant

    The English strings. Spread and override for another language. Type: CategoryPickerLabels.

Examples

Post an ad

The field with required and an error after submit: a popover on desktop, a full-height sheet on phones.

Sidebar with parents

List rows everywhere; selectableParents adds an “All in …” row to pick a whole branch.

Search every level

“phone” finds matches at any depth, leaves first, each with its path.

Browse links

Nodes with href render through the LinkProvider's link; here a pretend router.

Compact grid

Plain icons, size sm and four fixed columns.

Four levels deep

Cars › Car parts › Engine › Filters: the breadcrumb folds its middle.

Photo tiles

Nodes with an image instead of an icon, size lg.

Brand field

Outline, large, tone brand, parents selectable.

Dependencies

lucide-reactmotionbuttonfieldinputpopoveruse-controllable-stateuse-localeuse-media-querymotionnavigationnumber-formattext-matchtonesui-config

Files

  • components/ui/category-picker.tsx

FAQ

Is the React Category picker component free?

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

How do I install Category picker?

Run npx wingo-ui@latest add category-picker. It also adds the Wingo UI items it builds on (Button, Field, Input, Popover, useControllableState and 8 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/category-picker.

How does Category picker behave on phones?

The field shows the picked path in a form box and opens the picker in a popover on desktop and in a full-height bottom sheet on phones, where the sheet's title row doubles as the picker's header (the level's name and the back button appear in it as you drill in); picking a leaf closes it and returns focus to the field.

See also

Filter panelListing gridTree ViewListboxPopover

Used in

Marketplace Home PageFilter panelPost Listing Wizard

More React commerce components

Price rangeListing cardProduct galleryFavorite buttonOrder trackerCheckout steps
All 16 React commerce components
PreviousCart drawerNext Checkout steps

All categories

value: "" · no category picked