Skip to content

Cascader — spec

Citing Ant Design Cascader, MUI (no built-in — composition), shadcn-ui (no built-in)

Purpose

Multi-level select. User picks a leaf value by drilling through hierarchical levels. Used for: address (state → city → district), category trees (electronics → phones → models), org structure.

When Cascader vs alternatives

Pattern Use
Cascader Hierarchical with 2–4 fixed levels; user picks ONE leaf
Tree (with select) Variable depth, user might pick any node
Cascader → drill-in screens Mobile, when each level needs more space
Three Selects in a row When levels are independent (e.g., size, color, material — pick all three)

Anatomy

Trigger:                          Open (3-level cascade):
┌──────────────────────┐         ┌──────┬──────┬──────┐
│ 서울 / 강남구 / 역삼동  ▾│         │ 서울  │ 강남구│ 역삼동│
└──────────────────────┘         │ 부산  │ 서초구│ 청담동│
                                  │ 인천  │ 송파구│ 삼성동│
                                  └──────┴──────┴──────┘
Slot Required Notes
Trigger yes Shows full path or placeholder
Per-level panel yes Each level renders its options
Hover/click expand yes Hovering a parent expands its children
Search input optional Searches across all levels

API

<Cascader
  value={["seoul", "gangnam", "yeoksam"]}
  onValueChange={setValue}
  options={addressTree}
  separator=" / "
  searchable
/>

// where addressTree is:
type CascaderOption = {
  value: string;
  label: string;
  children?: CascaderOption[];
  disabled?: boolean;
  isLeaf?: boolean;
};
Prop Type Default Description
value string[] — Path to selected leaf (level values in order)
onValueChange (value: string[], path: CascaderOption[]) => void —
options CascaderOption[] — Hierarchical data
separator string " / " Display separator in trigger
placeholder string — When empty
searchable boolean false Cross-level search
expandTrigger "click" \| "hover" "click" How to expand levels
changeOnSelect boolean false Allow selection at non-leaf levels
showFullPath boolean true In trigger: full path vs leaf-only
loadData (option) => Promise<CascaderOption[]> — Async lazy-load children
disabled boolean false
error / errorText — —

Behavior

  • Click a parent: expands children to its right.
  • Click a leaf: selects, closes popover.
  • changeOnSelect: allows selecting at any level. Triggers onValueChange on every click.
  • Search mode: input at top of popover; results show full paths matching query, flattened.

States

State Visual
Default trigger Placeholder or value path
Open Popover with N panels visible
Hover row Highlighted; if has children, panel to right opens
Selected leaf Filled bg, checkmark
Loading (async) Spinner in current panel

Accessibility

  • Trigger: role="combobox", aria-expanded, aria-haspopup="tree", aria-controls.
  • Each panel: role="listbox" (or "tree" for accessibility tools).
  • Each option: role="option", aria-selected, aria-haspopup="listbox" if has children.

Keyboard

Key Behavior
Tab Reach trigger
Enter / Space Open popover
↑ / ↓ Move within current panel
→ Expand to next panel (if has children)
← Back to previous panel
Enter (on leaf) Select
Esc Close

Mobile pattern

Cascader's horizontal panel UI breaks on mobile. Switch to a drill-in screen pattern:

[Tap trigger] → [Level 1 list screen] → [Level 2 list] → [Level 3 list] → [back to form]

Each level is a full-screen list with a back button. Visual breadcrumb at top: "서울 > 강남구 > ..."

For Korean address: combine with Daum Postcode (more user-friendly for actual addresses) — see component-address-input.md.

Don't

  • Don't use Cascader for non-hierarchical multi-select. Use a Tree.
  • Don't use for 5+ levels — too deep; user gets lost.
  • Don't use for hierarchies that change frequently (categories that move). Tree handles dynamic shape better.
  • Don't combine changeOnSelect with deep hierarchies — user accidentally selects at level 1.

References

Cross-reference