Cascader

A select for a value that lives in a tree: each column narrows the next, and the value is the path rather than a single leaf.

Usage

When to use Cascader, what its value looks like, and the decisions it leaves to you.

When to use

Cascader is a Select for a value that lives in a tree. It opens one column per level, each column narrows the next, and what comes back is the whole path rather than the leaf you clicked.
Use it when the hierarchy is even and the reader always wants a leaf: country to city to district, category to subcategory to product, org to team to person. Every branch is the same depth, so every column reads the same way.
  • Use a TreeSelect when the hierarchy is uneven, or when a branch is a valid answer on its own. changeOnSelect allows that, but a tree whose branches end at different depths leaves columns half empty, and the reader unsure whether there is more to come.
  • Use two Selects when there are only two levels and the second is short. Two labeled fields are cheaper to read, cheaper to fill in on a phone, and a form library can validate each one on its own.
  • Use a Select when the options are flat.
  • Use a Menu or a Dropdown when picking is an action, not a value. Cascader is a form control: it holds a value and reports changes.

options and fieldNames

options is the whole tree, given at once: each node takes label, value and a children array, and a node with no children is a leaf. There is no depth limit, and the columns are rendered from whatever the active path reaches.
fieldNames renames those three keys so your data can stay the shape your API returns it in: { label: "name", value: "id", children: "kids" }. Only those three are mapped, and any you leave out keep their default, so a partial object is fine.
  • A node is identified by its path of values, so the same value under two different parents is fine, but not among siblings.
  • The value field you name in fieldNames also sets the type of value and onChange.
  • Keep the labels on one line. A column is a fixed width, and a label that wraps turns a tidy set of columns into three ragged blocks.
  • disabled on an option takes the whole branch with it: the flattened search list inherits a disabled ancestor, and the keyboard skips disabled options rather than landing on them.

The value is a path

value is an array with one entry per level, from the root. Choosing Berlin under Germany under Europe gives you ["eu", "de", "ber"], not "ber". This is the most common mistake with the component, and TypeScript catches it.
  • onChange gives you that array and, as a second argument, the matching option objects. Read the labels from the second argument instead of walking the tree again.
  • Store the path. Storing only the leaf means re-deriving its ancestors on every read, and it breaks outright the moment two branches share a leaf value.
  • A value with no matching option shows as the raw value in the field, so a saved path whose options have not loaded yet shows ids instead of names.

changeOnSelect

By default only a leaf is selectable: clicking a branch opens its column and changes nothing. changeOnSelect makes every node selectable, so a partial path is a legal value.
  • Turn it on when a broader answer is a real answer: “Germany” when the city is not known yet, a category without a subcategory.
  • Leave it off when a partial path is meaningless. Without it the reader cannot submit half an address by accident, which is the better default for most forms.
  • It changes when the popup closes, and it does it asymmetrically. With expandTrigger="click", clicking a branch selects it and leaves the popup open so the reader can keep going, and Enter on the same node selects and closes. With expandTrigger="hover" the click closes it too.
  • It also widens search. Without it the search list holds only leaf paths, and with it every path in between is searchable too.

multiple and showCheckedStrategy

multiple puts a checkbox on every option and turns the value from Value[] into Value[][], one array per selected path. The types change with it.
Checking a parent checks its whole subtree, so showCheckedStrategy decides which node of a fully checked branch is reported back to you.
  • Cascader.SHOW_PARENT, the default, reports a fully checked branch as its parent. Use it when the receiver understands “everything under here”.
  • Cascader.SHOW_CHILD lists the leaves instead. Use it when the receiver wants explicit ids, like a filter sent as a list.
  • Both constants come from @/components/ui/cascader with the component.
  • Each tag in the field shows only the last label of its path. See displayRender below, and the Accessibility tab for why that matters to a screen reader.
showSearch flattens the tree into one column of paths. A match shows its labels joined with  / , so a row reads “Europe / Germany / Berlin”, since a leaf name alone is often ambiguous.
  • The default filter tests the whole path, so typing a parent's name matches all of its leaves.
  • Pass an object rather than true to reach filter, render, sort, limit and autoClearSearchValue. The top-level searchValue and onSearch props are deprecated in favor of the same keys inside this object.
  • limit is 50 by default and limit: false lifts it. Lifting it on a large tree renders every match, so pair it with a tree small enough to survive that.
  • The columns are hidden while a search runs, and so are the left and right keys between them. Up, down and Enter still work.
  • Search does not trigger loadData: a lazily loaded tree is only searchable as far as it has been expanded. If the tree has to be searchable in full, load it in full, or search server side and feed options from the result.

loadData for lazy levels

loadData fetches the next level on demand. It is called with the option objects of the path just opened. Set children on the last of them, then pass a new copy of the top-level options array so the tree re-renders.
  • The spinner clears when the option it was given has grown a children key. A loader that returns data without assigning it leaves the column spinning forever with nothing logged.
  • An empty level still needs a marker. Assign an empty array, or set isLeaf: false, so the component can tell “nothing here” from “still loading”.
  • Mark real leaves with isLeaf so the last column does not offer a drill-in chevron into a level that does not exist.
  • A lazy tree cannot restore a saved path on its own: the options for the deeper cells do not exist yet, so seed them alongside the value or accept that the trigger shows raw ids until they arrive.

displayRender and expandTrigger

displayRender sets the text in the closed field. It gets the path's labels and, as a second argument, the option objects behind them. The default joins the labels with  / , and in multiple mode keeps only the last label per tag, which is where a custom renderer helps most.
  • Shorten a long path here rather than by truncating with CSS: the renderer knows which cells matter and a text overflow does not.
  • You can return a node, like a tag or a flag with the text, but keep the words in it. The field is what a screen reader reads back.
  • optionRender does the same for one row inside a column of the popup. The two props are independent.
expandTrigger decides what opens the next column. The default is click, which also works on touch, where there is no hover.
  • hover is faster for a deep tree on a mouse, and it is the wrong default for a form a phone will ever see.
  • Pick one and keep it across a view, since it changes how a click behaves.
The popup renders in a portal, so a descendant selector on the field does not reach it. Style it through classNames.popup.root.