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

Inputs

Tree Select

Inputs

Tree Select React component for Tailwind CSS v4

Timezone SelectAddress Input
Live props
Behavior
multiple
searchable
selectableFolders
cascade
checkStrategy
defaultExpanded
clearable
responsive
State
defaultValue
invalid
disabled
readOnly
defaultOpen
Content
placeholder
showPath
pathSeparator
sheetTitle
Field
label
description
error
required
name
Layout
maxTagCount2
wrapTags
side
align
maxHeight360px
Appearance
guides
folderIcons
size
variant
radius
tone
color
Code
import TreeSelect from "@/components/ui/tree-select"

<TreeSelect />
  1. Home
  2. Components
  3. Inputs
  4. Tree Select
Pro

Tree Select React component for Tailwind CSS v4

v1.0.0·Added in 1.14.0

A React tree select: a field that opens a searchable tree of categories, folders or locations, for one value or many as removable tags.

Click or tap the field and a tree opens under it with the search row focused. Type to filter: matches keep their parents, the path to them opens and the matched letters turn bold. Click a row to pick it and the list closes; clicking a folder only opens it unless selectableFolders is on (then the chevron opens it). With multiple, each row has a checkbox and the list stays open: checking a folder checks its whole branch, a folder turns to a dash when only some of its children are checked, the trigger shows removable tags up to maxTagCount and then +N, and the footer counts and clears. Keyboard: on the closed field Enter, Space, ArrowDown or ArrowUp open it and a letter opens it with that letter in the search; Backspace removes the last tag (or clears a single value with clearable). In the search ArrowDown or Enter move into the tree. In the tree ArrowDown and ArrowUp move (ArrowUp on the first row returns to the search), ArrowRight opens a folder or steps into it, ArrowLeft closes it or jumps to the parent, Home and End jump, typing letters jumps to the next row that starts with them, Enter picks a row (or opens a folder), Space picks or checks, Escape closes and puts focus back on the field, and Tab moves on from the field. On a phone the list opens as a bottom sheet with the search pinned under the title and 44px rows; with multiple a Done button closes it.

Tree Select is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add tree-select, together with Tree View, Field, Drawer, Tag, Button and 5 more and the npm packages @radix-ui/react-popover, motion and lucide-react.

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 tree-select

Tree Select 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 TreeSelect from @/components/ui/tree-select.

import { useState } from "react"
import TreeSelect, { type TreeSelectNode } from "@/components/ui/tree-select"

const categories: TreeSelectNode[] = [
  {
    id: "electronics",
    label: "Electronics",
    children: [
      { id: "phones", label: "Phones", children: [{ id: "iphone", label: "iPhone" }, { id: "android", label: "Android" }] },
      { id: "laptops", label: "Laptops" },
    ],
  },
  { id: "home", label: "Home & Kitchen", children: [{ id: "furniture", label: "Furniture" }] },
]

export function ProductCategory() {
  const [category, setCategory] = useState("android")
  const [filters, setFilters] = useState<string[]>(["phones"])
  return (
    <div className="grid gap-4">
      <TreeSelect label="Category" data={categories} value={category} onValueChange={setCategory} showPath />
      <TreeSelect label="Show products in" data={categories} multiple value={filters} onValueChange={setFilters} clearable />
    </div>
  )
}

Props

  • data*
    TreeSelectNode[]

    The tree: { id, label, textValue?, icon?, description?, disabled?, children? }. Ids must be unique across the whole tree; a label that is not a string needs textValue for the search, the tags and the path.

  • multipledefault false
    boolean

    Several values as removable tags, with checkboxes in the tree. The value becomes a string[].

  • value
    string | string[]

    The chosen id ("" is nothing), or ids with multiple (controlled). Pair it with onValueChange.

  • defaultValuedefault ""
    string | string[]

    The starting value while uncontrolled ([] with multiple). A form reset returns to it.

  • onValueChange
    (value: string) => void | (value: string[]) => void

    Called with the new id, or the new list with multiple.

  • placeholderdefault "Select an option"
    string

    Shown while nothing is chosen. Also titles the phone sheet when there is no label.

  • labeldefault ""
    ReactNode

    The label above the field; plain text also names the tree and titles the phone sheet.

  • descriptiondefault ""
    ReactNode

    Helper text under the field, linked with aria-describedby.

  • errordefault ""
    ReactNode

    Replaces the description with an error and marks the field invalid.

  • requireddefault false
    boolean

    Adds the asterisk and aria-required; with name, an empty field blocks the form submit.

  • searchabledefault true
    boolean

    A search row on top of the tree. Matches keep their parents, the path to them opens and the matched letters turn bold; accents never matter.

  • selectableFoldersdefault false
    boolean

    Folders can be picked too (single), or checked on their own (multiple without cascade). Off: a folder row only opens and closes.

  • cascadedefault true
    boolean

    Multiple only: checking a folder checks its branch and a fully checked branch checks its folder. Off: every row checks on its own.

  • checkStrategydefault "parent"
    "all" | "parent" | "child"

    Multiple with cascade: what the value holds. parent keeps the topmost fully checked folder instead of its children, child keeps only the leaves, all keeps every checked id.

  • defaultExpandeddefault false
    boolean | string[]

    Folders open on first render: true opens every folder, or pass ids. The path to the chosen value always opens with the list.

  • showPathdefault false
    boolean

    The field shows the whole path (Electronics / Phones / Android). The folders truncate first so the picked name stays readable; tags keep the path in their title.

  • pathSeparatordefault " / "
    string

    Between the names of the path.

  • clearabledefault false
    boolean

    An x in the field that clears the value. It only shows while something is chosen.

  • maxTagCountdefault 2
    number

    Multiple only: tags shown before a +N tag (its title lists the rest). 0 shows every tag.

  • wrapTagsdefault false
    boolean

    Multiple only: tags wrap onto more lines and the field grows, instead of one clipped row.

  • guidesdefault true
    boolean

    Vertical indent guides in the tree.

  • folderIconsdefault false
    boolean

    Folder and file icons for nodes that have no icon of their own.

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

    Field height 32 / 40 / 48px (44px on touch for md), the tree rows and the tags follow.

  • variantdefault "soft"
    "soft" | "outline" | "underline" | "ghost"

    The field look, the same as every input and select.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape, scaled with the size like the button (md is 8 / 10 / 12px). Tags follow it.

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

    The checkboxes, the picked row, the tags and the sheet's Done button.

  • colordefault ""
    string

    Any CSS color; replaces the tone. Leave empty to use the tone.

  • invaliddefault false
    boolean

    The red ring and aria-invalid without a message (error sets it too).

  • disableddefault false
    boolean

    Dims the field and blocks it.

  • readOnlydefault false
    boolean

    Shows the value and keeps it focusable, but the list does not open and tags cannot be removed.

  • responsivedefault true
    boolean

    A bottom sheet below 768px, with the search pinned under the title, 44px rows and (multiple) a Done button.

  • sheetTitledefault ""
    string

    The phone sheet's title. Default: the label's text, then aria-label, then the placeholder.

  • sidedefault "bottom"
    "top" | "bottom"

    Where the popover opens; it flips when there is no room.

  • aligndefault "start"
    "start" | "center" | "end"

    Popover alignment against the field.

  • maxHeightdefault 360
    number

    The popover's maximum height in px (at least 160); it never grows past the room on screen.

  • namedefault ""
    string

    A hidden input per value with this name, so the field submits with a form (FormData.getAll(name)).

  • id
    string

    The trigger's id; the label points to it.

  • leftIcon
    ReactNode

    An icon at the start of the field.

  • open
    boolean

    Controlled open state. Pair it with onOpenChange.

  • defaultOpendefault false
    boolean

    Starts with the list (or the sheet) open. Pass open and onOpenChange to control it.

  • onOpenChange
    (open: boolean) => void

    Called when the list opens or closes.

  • labels
    Partial<TreeSelectLabels>

    Every string: { search: "Search", clearSearch: "Clear search", noResults: "No matches", clear: "Clear", done: "Done", remove: "Remove", more: (n) => "+n", selected: (n) => "n selected", tree: "Options" }.

  • container
    HTMLElement | null

    Portal target for the popover and the sheet (default document.body).

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, hideLabel.

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "box" | "trigger" | "value" | "path" | "placeholder" | "tags" | "tag" | "overflow" | "clear" | "chevron" | "content" | "search" | "tree" | "footer", string>>

    Extra classes for the inner parts.

  • className
    string

    Merged onto the Field root when there is a label, description or error, otherwise onto the box.

  • ref
    RefObject<HTMLButtonElement | null> or (node) => void

    The trigger button; other button props (onBlur, aria-*, data-*, form) land there too.

Parts

  • TreeSelectNode

    type

    One node of data: { id, label, textValue?, icon?, description?, disabled?, children? }.

  • TreeSelectProps

    type

    The props, with value typed as a string or, with multiple, a string[].

  • TreeSelectLabels

    type

    The shape of the labels prop.

  • DEFAULT_TREE_SELECT_LABELS

    constant

    The English labels; spread them to translate a few strings.

  • getTreeSelectPath

    function

    The names from the root down to a node: getTreeSelectPath(data, "android") gives ["Electronics", "Phones", "Android"]. Handy for a summary or a breadcrumb.

    • data*
      TreeSelectNode[]

      The tree.

    • id*
      string

      The node; an unknown id gives [].

  • treeSelectText

    function

    A node's plain text: textValue, else a string label, else the id.

Examples

With the path

showPath shows Electronics / Laptops / Windows laptops and clearable adds an x.

Several categories

multiple turns rows into checkboxes and values into tags; a folder checks its whole branch.

Check strategies

The same picks reported three ways: every id, the topmost folders, or the leaves only.

Pick a department or a team

selectableFolders lets a folder be the value; the chevron opens it.

Wrapping tags

checkStrategy="child" keeps cities only, maxTagCount={0} and wrapTags show every tag.

Independent checks

cascade={false} with selectableFolders: each folder is its own choice, in the info tone.

Sizes and states

sm, md and lg line up with every field and button; disabled and invalid below.

In a form

required, a validation error and a hidden input named category.

Dependencies

@radix-ui/react-popovermotionlucide-reacttree-viewfielddrawertagbuttonuse-controllable-stateuse-media-querymotiontonesui-config

Files

  • components/ui/tree-select.tsx

FAQ

Is the React Tree Select component free?

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

How do I install Tree Select?

Run npx wingo-ui@latest add tree-select. It also adds the Wingo UI items it builds on (Tree View, Field, Drawer, Tag, Button and 5 more) and the npm packages @radix-ui/react-popover, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/tree-select.

How does Tree Select behave on phones?

A bottom sheet below 768px, with the search pinned under the title, 44px rows and (multiple) a Done button. The responsive prop controls this and is on by default.

See also

Tree ViewMulti SelectSelectComboboxTag Input

More React input components

Duration PickerOTP InputAvatar UploadColor PickerCalendarSlider
All 40 React input components
PreviousTimezone SelectNext Address Input

New product

Pixel 9 Pro · SKU 40812

Select an option

Where the product shows in your store.

category=""

bottom start · 360px

Try typing “chairs”, “mac” or “audio” · on phones: a “Category” bottom sheet