Cascader

Select a path from hierarchical data with cascading columns

Usage

Cascader lets users select a value from hierarchical data: picking an option in one column reveals its children in the next column. The value is a path from the root option to the selected one, for example ['asia', 'jp', 'tokyo'].

All demos on this page switch to the flat list layout on small screens, open them on a larger screen to see the columns.

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 array of option values or null when nothing is selected. onChange is called with the selected path and the corresponding 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. Clicking a parent option then both selects it and expands the next column:

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. By default, the dropdown stays open, unless allowDeselect is set to false:

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}
    />
  );
}

With expandTrigger="hover", the open column stays in place while the cursor moves diagonally toward it. Set safeAreaPolygon={false} to disable this behavior, or pass an object to configure Floating UI safePolygon options:

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"
      safeAreaPolygon={false}
      label="Location"
      placeholder="Hover to expand"
      data={data}
    />
  );
}

Flat list layout

Columns require horizontal space and do not fit narrow screens. Set withColumns={false} to render options as a flat list of full paths instead:

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}
    />
  );
}

Combine withColumns with the use-matches hook to render columns on desktop and the flat list on mobile:

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

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

Max displayed levels

Use the maxDisplayedLevels prop (3 by default) to limit the number of columns displayed next to each other. When the limit is exceeded, the first columns are hidden and a control to reveal them is displayed on the left:

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 (260 by default) to set the max height of a column:

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:

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 scrollAreaProps to pass props down to the ScrollArea of 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 { useState } from 'react';
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