<!-- Protocore Design System — Cascader -->
# Cascader

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/cascader`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Cascader } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/cascader

> Field that opens cascading columns to drill down a hierarchy of nested options, returning the full path of chosen values.

## When to use it

Use **Cascader** for a strict path through a deep, uniform hierarchy — country → region → city, or category → subcategory → product. The column layout keeps each level's options short. When the hierarchy is browsed as a whole (expanding several branches at once) use **TreeSelect**; for a flat list use **Select**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `changeOnSelect` | `boolean` | no | `false` | Also commit when a branch (not just a leaf) is selected. |
| `className` | `string` | no | — | — |
| `defaultOpen` | `boolean` | no | — | Initial open state in uncontrolled mode. |
| `defaultValue` | `string[]` | no | — | Initial selected path in uncontrolled mode. |
| `disabled` | `boolean` | no | — | Disable the whole control. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Fires when the overlay opens or closes. |
| `onValueChange` | `((path: string[], options: CascaderOption[]) => void)` | no | — | Fires with the next path and the matching option chain. |
| `open` | `boolean` | no | — | Controlled open state of the overlay. |
| `options` | `CascaderOption[]` | yes | — | The option forest. |
| `placeholder` | `string` | no | `Select` | Text shown when nothing is selected. |
| `separator` | `string` | no | `/` | Separator between labels in the trigger. |
| `size` | `enum` | no | `md` | Field height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `string[]` | no | — | Controlled selected path (values from root to leaf). |

## Examples

### Basics

Each column reveals the children of the option chosen in the column before it. Selecting a leaf commits the whole path and closes the overlay. The value is a `string[]` of option values; controllable via `value` / `defaultValue` / `onValueChange`.

```tsx
import { useState } from "react";
import { Cascader, type CascaderOption } from "@protocore/pds";

const OPTIONS: CascaderOption[] = [
  {
    value: "eu",
    label: "Europe",
    children: [
      {
        value: "ro",
        label: "Romania",
        children: [
          { value: "b", label: "Bucharest" },
          { value: "cj", label: "Cluj-Napoca" },
        ],
      },
      {
        value: "fr",
        label: "France",
        children: [
          { value: "paris", label: "Paris" },
          { value: "lyon", label: "Lyon" },
        ],
      },
    ],
  },
  {
    value: "na",
    label: "North America",
    children: [
      { value: "us", label: "United States", children: [{ value: "nyc", label: "New York" }] },
    ],
  },
];

export default function Basics() {
  const [value, setValue] = useState<string[]>(["eu", "ro", "b"]);

  return (
    <div style={{ width: 280 }}>
      <Cascader aria-label="Location" options={OPTIONS} value={value} onValueChange={setValue} />
    </div>
  );
}
```

### Change on select

Set `changeOnSelect` to commit at every level, not just leaves — useful when an intermediate node (a whole region, a whole category) is itself a valid answer.

```tsx
import { useState } from "react";
import { Cascader, type CascaderOption } from "@protocore/pds";

const OPTIONS: CascaderOption[] = [
  {
    value: "hw",
    label: "Hardware",
    children: [
      { value: "phones", label: "Phones", children: [{ value: "flagship", label: "Flagship" }] },
      { value: "laptops", label: "Laptops" },
    ],
  },
  {
    value: "sw",
    label: "Software",
    children: [{ value: "apps", label: "Apps" }],
  },
];

export default function ChangeOnSelect() {
  const [value, setValue] = useState<string[]>(["hw"]);

  return (
    <div style={{ width: 280 }}>
      <Cascader
        aria-label="Category"
        options={OPTIONS}
        value={value}
        onValueChange={setValue}
        changeOnSelect
      />
    </div>
  );
}
```

## Do & don't

**Do**

- Give the trigger an accessible name via a `Field` label or `aria-label`.
- Keep each level's option set short enough to scan in a column.
- Use `changeOnSelect` when an intermediate node is a valid final answer.
- Read the ordered `path` (and resolved option chain) from `onValueChange`.

**Don't**

- Don't use it for a flat list — a Select is simpler and faster.
- Don't nest so deep that the columns overflow the viewport.
- Don't leave the trigger unlabelled when no visible field label wraps it.
- Don't reconstruct the path from the trigger text — read the `string[]`.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Enter / Space` | Open the overlay, or select the focused option. |
| `Arrow Up / Down` | Move within the current column. |
| `Arrow Right` | Descend into the focused option's children. |
| `Arrow Left` | Return to the parent column. |
| `Esc` | Close the overlay without changing the value. |

**Notes**

- The overlay is a Radix Popover: portalled, dismissed on outside-click and Escape, and returns focus to the trigger on close.
- Each column is a `role="listbox"` of `role="option"` buttons with `aria-selected` on the active option; a roving tabindex keeps one option tabbable.
- `invalid` sets `aria-invalid` on the trigger; the trigger exposes `aria-expanded` for its open state.

## Related

`tree-select`, `select`, `multi-select`, `combobox`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
