Cascader

Select a path from hierarchical data with cascading columns

Usage

Cascader lets users select a value from hierarchical data by drilling down through cascading columns: picking an option in one column reveals its children in a new column to the right. The value is an ordered path from the root option to the selected node, for example ['asia', 'jp', 'tokyo'].

Cascader is useful for large hierarchical datasets where a nested dropdown would be hard to navigate – locations (continent → country → city), categories, organization structures, etc.

All demos on this page switch to the flat list layout on small screens, so the cascading columns and interactions like hover expand and keyboard navigation cannot be fully tested on mobile devices – open the demos on a larger screen to try them out.

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  // Switch to a flat list on small screens
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

Data prop

Data passed to the data prop is an array of CascaderOption objects:

  • Each option must have a unique value across the whole data tree
  • label is optional, value is used when it is not set
  • children contains an array of nested options
  • disabled prevents an option from being selected or expanded
import { CascaderOption } from '@mantine/core';

const data: CascaderOption[] = [
  {
    value: 'asia',
    label: 'Asia',
    children: [
      {
        value: 'jp',
        label: 'Japan',
        children: [{ value: 'tokyo', label: 'Tokyo' }],
      },
    ],
  },
];

Value and onChange

Cascader value is an ordered array of option values (a path from root to node) or null when nothing is selected. The onChange callback is called with the selected path and the resolved chain of CascaderOption objects:

import { useState } from 'react';
import { Cascader, CascaderOption } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState<string[] | null>(['europe', 'fr', 'paris']);

  const handleChange = (path: string[] | null, options: CascaderOption[]) => {
    setValue(path);
    console.log(path, options);
  };

  return <Cascader data={data} value={value} onChange={handleChange} />;
}

By default, only leaf options (options without children) can be selected. Clicking a parent option only expands the next column.

Change on select

Set the changeOnSelect prop to allow selecting any option, not only leaves. When enabled, clicking a parent option both selects its path and expands the next column, so any intermediate level is a valid value:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      changeOnSelect
      label="Location"
      placeholder="Pick any level"
      data={data}
    />
  );
}

Close on select

closeOnSelect controls whether the dropdown is closed after a value is selected. When it is not set, it defaults to the opposite of allowDeselect: the dropdown stays open by default (so the selection can be toggled off), unless allowDeselect={false} is set. Set closeOnSelect explicitly to override this behavior:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      closeOnSelect={false}
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

Allow deselect

By default, clicking the selected option again removes the value. Set allowDeselect={false} to disable this behavior:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      allowDeselect
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Expand trigger

By default, the next column is opened when an option is clicked. Set expandTrigger="hover" to open the next column on hover instead – selecting a leaf still requires a click:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      expandTrigger="hover"
      label="Location"
      placeholder="Hover to expand"
      data={data}
    />
  );
}

Flat list layout

Cascading columns require horizontal space and are not ideal for narrow screens, which makes them a poor fit for mobile layouts. Set withColumns={false} to render options as a flat list of full paths (the same way as search results) instead of columns – this layout works well on mobile:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      withColumns={false}
      searchable
      label="Location"
      placeholder="Pick location"
      nothingFoundMessage="Nothing found..."
      data={data}
    />
  );
}

The most common use case is to render columns on desktop and the flat list on mobile. Combine withColumns with the use-matches hook to switch between the two layouts depending on the screen size. All demos on this page use this technique – resize the window to see the layout change:

import { Cascader, useMatches } from '@mantine/core';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return <Cascader withColumns={withColumns} data={data} />;
}

Max displayed levels

Deep hierarchies can produce many columns. Use the maxDisplayedLevels prop (3 by default) to limit how many columns are displayed next to each other. When you drill deeper than the limit, the earliest columns are replaced by the deeper ones and a control is displayed on the left. Click it to reveal the previous levels; a control on the right then lets you return to the deeper levels:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      maxDisplayedLevels={2}
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Searchable

Set the searchable prop to allow filtering options. In search mode, the columns are replaced with a flat list of full paths that match the query:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      searchable
      label="Location"
      placeholder="Search location"
      nothingFoundMessage="Nothing found..."
      data={data}
    />
  );
}

Use the filter prop to customize how paths are matched and renderSearchOption to customize how search results are rendered:

import { Cascader, CascaderOption } from '@mantine/core';

function Demo() {
  return (
    <Cascader
      searchable
      data={data}
      filter={(query, options: CascaderOption[]) =>
        options.some((option) => String(option.label).toLowerCase().includes(query.toLowerCase()))
      }
      renderSearchOption={(query, options) => options.map((option) => option.label).join(' → ')}
    />
  );
}

Controlled search value

Use searchValue and onSearchChange props to control the search value:

import { useState } from 'react';
import { Cascader } from '@mantine/core';

function Demo() {
  const [searchValue, setSearchValue] = useState('');
  return (
    <Cascader
      searchable
      data={data}
      searchValue={searchValue}
      onSearchChange={setSearchValue}
    />
  );
}

Nothing found

Set the nothingFoundMessage prop to display a message when no options match the search query:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      searchable
      label="Location"
      placeholder="Search location"
      nothingFoundMessage="Nothing found..."
      data={data}
    />
  );
}

Display value

By default, the selected path is displayed in the input as option labels joined by the separator (/ by default). Use the separator prop to change the divider and the formatValue prop to fully customize the input label:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      separator="›"
      defaultValue={['asia', 'jp', 'tokyo']}
      formatValue={({ options }) => options.map((option) => option.label).join(' › ')}
      data={data}
    />
  );
}

Column width

Use the columnWidth prop to set a fixed width for every column and maxDropdownHeight to control the height of a column before it becomes scrollable (260 by default):

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      columnWidth={140}
      maxDropdownHeight={200}
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

Custom option rendering

Use the renderOption prop to customize how options are rendered in columns. The function receives the option and its column level:

import { Badge, Cascader, CascaderOption, CascaderProps, Group, useMatches } from '@mantine/core';
import { data } from './data';

const flags: Record<string, string> = {
  jp: '🇯🇵', kr: '🇰🇷', fr: '🇫🇷', de: '🇩🇪', us: '🇺🇸', ca: '🇨🇦',
};

function countCities(option: CascaderOption): number {
  if (!option.children || option.children.length === 0) {
    return 1;
  }
  return option.children.reduce((acc, child) => acc + countCities(child), 0);
}

// Regions display the number of cities, countries display a flag, cities display nothing extra
const renderCascaderOption: CascaderProps['renderOption'] = (option, level) => (
  <Group gap="xs" justify="space-between" wrap="nowrap" flex="1">
    <Group gap={6} wrap="nowrap">
      {level === 1 && <span>{flags[option.value]}</span>}
      <span>{option.label}</span>
    </Group>
    {level === 0 && (
      <Badge size="xs" variant="light" color="gray">
        {countCities(option)} cities
      </Badge>
    )}
  </Group>
);

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      renderOption={renderCascaderOption}
      data={data}
    />
  );
}

Check icon

Use the withCheckIcon prop to toggle the check icon on the selected option and checkIconPosition to change its position (left or right):

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      checkIconPosition="left"
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Disabled options

Set the disabled property on an option to prevent it from being selected or expanded. Disabled options are skipped during keyboard navigation:

import { Cascader, CascaderOption, useMatches } from '@mantine/core';

const data: CascaderOption[] = [
  {
    value: 'asia',
    label: 'Asia',
    children: [
      { value: 'jp', label: 'Japan', children: [{ value: 'tokyo', label: 'Tokyo' }] },
      { value: 'kr', label: 'South Korea', disabled: true },
    ],
  },
  { value: 'antarctica', label: 'Antarctica', disabled: true },
];

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

Left and right sections

Cascader supports leftSection and rightSection props the same way as other inputs. These sections are usually used to add icons:

import { MapPinIcon } from '@phosphor-icons/react';
import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      leftSectionPointerEvents="none"
      leftSection={<MapPinIcon size={16} />}
      data={data}
    />
  );
}

Clearable

Set the clearable prop to display the clear button when a value is selected:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      clearable
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Use the clearSectionMode prop to control how the clear button and rightSection are rendered:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      clearable
      clearSectionMode="clear"
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Scrollable columns

Each column is scrollable. Use the maxDropdownHeight prop to control the height of a column before it becomes scrollable and scrollAreaProps to pass props down to the ScrollArea in each column:

import { Cascader, CascaderOption } from '@mantine/core';

const cities = Array.from({ length: 30 }, (_, index) => ({
  value: `city-${index + 1}`,
  label: `City ${index + 1}`,
}));

const data: CascaderOption[] = [
  { value: 'asia', label: 'Asia', children: [{ value: 'jp', label: 'Japan', children: cities }] },
  { value: 'europe', label: 'Europe', children: [{ value: 'fr', label: 'France', children: cities }] },
];

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp']}
      maxDropdownHeight={180}
      data={data}
    />
  );
}

Control dropdown opened state

Use dropdownOpened, defaultDropdownOpened, onDropdownOpen and onDropdownClose props to control the dropdown opened state:

import { Cascader } from '@mantine/core';

function Demo() {
  const [dropdownOpened, setDropdownOpened] = useState(false);
  return (
    <Cascader
      data={data}
      dropdownOpened={dropdownOpened}
      onDropdownOpen={() => setDropdownOpened(true)}
      onDropdownClose={() => setDropdownOpened(false)}
    />
  );
}

Dropdown position

Use comboboxProps to pass props down to the underlying Combobox and Popover. For example, set position to change the dropdown position:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ position: 'top-start' }}
      data={data}
    />
  );
}

Dropdown offset

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ offset: 0 }}
      data={data}
    />
  );
}

Dropdown width

In flat list mode (withColumns={false}) the dropdown width can be controlled with comboboxProps={{ width }}. In columns mode the dropdown is sized to the columns:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      withColumns={false}
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ width: 220, position: 'bottom-start' }}
      data={data}
    />
  );
}

Dropdown padding

import { Cascader } from '@mantine/core';
import { data } from './data';

// Dropdown padding is only applied to the flat list (withColumns={false});
// in columns mode each column manages its own padding.
function Demo() {
  return (
    <Cascader
      withColumns={false}
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ dropdownPadding: 12 }}
      data={data}
    />
  );
}

Dropdown shadow

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ shadow: 'md' }}
      data={data}
    />
  );
}

Dropdown animation

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      comboboxProps={{ transitionProps: { transition: 'pop', duration: 200 } }}
      data={data}
    />
  );
}

Inside Popover

To use Cascader inside a Popover, set comboboxProps={{ withinPortal: false }}:

import { Button, Cascader, Popover } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Popover width={320} position="bottom" withArrow shadow="md">
      <Popover.Target>
        <Button>Toggle popover</Button>
      </Popover.Target>
      <Popover.Dropdown>
        <Cascader
          withColumns={false}
          label="Location"
          placeholder="Pick location"
          comboboxProps={{ withinPortal: false }}
          data={data}
        />
      </Popover.Dropdown>
    </Popover>
  );
}

Combobox props

You can override Combobox props with comboboxProps. This is useful when you need to change some of the props that are not exposed by Cascader, for example withinPortal:

import { Cascader } from '@mantine/core';

function Demo() {
  return <Cascader comboboxProps={{ withinPortal: false }} data={[]} />;
}

Change dropdown z-index

import { Cascader } from '@mantine/core';

function Demo() {
  return <Cascader comboboxProps={{ zIndex: 1000 }} data={[]} />;
}

Styles API

Use the Styles API to customize Cascader styles:

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Left and right sections

Cascader supports leftSection and rightSection props. These sections are rendered with absolute positioning inside the input wrapper. You can use them to display icons, input controls, or any other elements.

You can use the following props to control sections styles and content:

  • rightSection / leftSection – React node to render on the corresponding side of input
  • rightSectionWidth/leftSectionWidth – controls the width of the right section and padding on the corresponding side of the input. By default, it is controlled by the component size prop.
  • rightSectionPointerEvents/leftSectionPointerEvents – controls the pointer-events property of the section. If you want to render a non-interactive element, set it to none to pass clicks through to the input.

Input props

Cascader component supports Input and Input.Wrapper component features and all input element props. The Cascader documentation does not include all features supported by the component – see the Input documentation to learn about all available features.

Input description

Variant
Size
Radius
import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';
function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      label="Input label"
      description="Input description"
      withColumns={withColumns}
      placeholder="Pick location"
      data={data}
    />
  );
}

Read only

Set readOnly to make the input read only. When readOnly is set, Cascader will not open the dropdown and will not call the onChange function.

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      readOnly
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp', 'tokyo']}
      data={data}
    />
  );
}

Disabled

Set disabled to disable the input. When disabled is set, the user cannot interact with the input and Cascader will not open the dropdown.

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      disabled
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

Error state

Pick a valid location

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      error="Pick a valid location"
      data={data}
    />
  );
}

Success state

Looks good!

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      defaultValue={['asia', 'jp', 'tokyo']}
      success="Looks good!"
      data={data}
    />
  );
}

Loading state

Set the loading prop to display a loader in the right section instead of the chevron:

import { Cascader } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <Cascader
      label="Location"
      placeholder="Pick location"
      loading
      data={data}
    />
  );
}

Accessibility

Cascader columns are rendered as nested listboxes with option elements. To make the component accessible, set the label prop or pass aria-label to the component:

import { Cascader } from '@mantine/core';

// Set aria-label when the input does not have a visible label
function Demo() {
  return <Cascader data={data} aria-label="Pick location" />;
}

Keyboard interactions

KeyDescription
ArrowDown/ArrowUpOpens the dropdown, then moves between options in the current column
ArrowRightExpands the highlighted option into the next column
ArrowLeftCollapses the current column and moves back to the previous one
EnterExpands a parent option or selects the highlighted leaf option
EscapeCloses the dropdown