首页
/ Ant Design Cascader 语义化结构样式定制全解:classNames 与 styles 的对象 / 函数用法

Ant Design Cascader 语义化结构样式定制全解:classNames 与 styles 的对象 / 函数用法

2026-09-06 18:32:34作者:温艾琴Wonderful

Cascader 级联选择器在 Ant Design 中被渲染成「选择框容器 + 输入框 + 前缀 / 后缀 + 多选标签 + 弹出菜单」等多层语义化 DOM。本文围绕 style-class.md 示例 的说明,系统讲解如何通过 classNamesstyles 以「对象」或「函数」形式精准定制每一层结构的 class 与行内样式,并结合仓库源码与测试用例说明其合并原理、函数入参以及废弃 API 的迁移写法,帮助你在业务中摆脱全局 CSS 覆盖,直接按语义节点做精细化定制。

一、什么是 Cascader 的语义化结构(Semantic DOM)

在介绍两个 Props 之前,需要先理解「语义化结构」这一概念。Cascader 复用了 Select 体系的 DOM 骨架,从源码中导出的 CascaderSemanticType(见 components/cascader/index.tsx#L57-L82)可以看到,整个组件被拆成了若干可寻址的语义节点:

语义节点 类型 说明
root string | CSSProperties 根容器,包含相对定位、行内 flex 布局、光标、过渡动画、边框等选择器容器基础样式
prefix string | CSSProperties 前缀元素,承载前缀内容的布局与样式
suffix string | CSSProperties 后缀元素,承载清除按钮、箭头图标等后缀内容
input string | CSSProperties 输入框元素,承载搜索输入框样式、光标控制、字体继承等
placeholder string | CSSProperties 占位符文本元素的字体样式与颜色
content string | CSSProperties 选择内容展示区域(多选时同时容纳标签)
item string | CSSProperties 多选模式下每个已选标签元素(边框、背景、内边距等)
itemContent string | CSSProperties 多选标签内部文字区域(省略号样式)
itemRemove string | CSSProperties 多选标签的移除按钮样式
popup 嵌套对象 弹出浮层,进一步拆分为 popup.root(浮层容器:定位、z-index、背景、阴影)、popup.list(选项列表容器:布局、滚动、最大高度)、popup.listItem(选项条目:内边距、悬浮 / 选中 / 禁用态)

其中 popup 节点是嵌套结构,其内部字段对齐 Select 组件的 SelectSemanticType(见 index.tsx 的 import 与类型引用)。官方文档的交互式结构预览正是由 demo/_semantic.tsx 调用 .dumi/theme/common/SelectSemanticTemplate 渲染的,可在单选 / 多选两种模式下逐层查看每个节点对应的 DOM 区域。

理解了节点划分,classNamesstyles 的作用就很清晰了:

  • classNames:把自定义 class 挂到上述某个(或某层)语义节点上;
  • styles:把内联 CSSProperties 注入上述节点;
  • 两者都既支持传普通对象,也支持传接收 { props } 的函数(文档原文即:通过 classNamesstyles 传入对象/函数可自定义 Cascader 的语义化结构样式)。

二、对象形式:直接给节点挂 class / 注入行内样式

1. 用对象 styles 定制前缀与后缀

demo/style-class.tsx 中,第一只 Cascader 演示了对象形式:传入 prefix="🏠" 增加前缀图标,再通过对象 styles 把前缀 / 后缀颜色统一调成浅灰色:

const stylesObject: CascaderProps['styles'] = {
  prefix: {
    color: '#ccc',
  },
  suffix: {
    color: '#ccc',
  },
};

<Cascader
  options={options}
  onChange={onChange}
  placeholder="Object styles"
  classNames={classNames}
  styles={stylesObject}
  prefix="🏠"
/>

这里的键名必须属于上一节列出的语义节点之一,值则是一组 React.CSSProperties,等价于代码中直接往对应 DOM 上写 style

2. 用对象 classNames 定制根容器圆角

classNames 的键与 styles 一致,值是一个字符串类名。示例使用 antd-stylecreateStyles 产出 CSS,再把这些类名按语义节点分发:

const useStyles = createStyles(({ token }) => ({
  root: {
    borderRadius: token.borderRadiusLG,
  },
}));

const { styles: classNames } = useStyles();

<Cascader
  options={options}
  placeholder="Object styles"
  classNames={classNames} // { root: '生成的类名' }
  styles={stylesObject}
  prefix="🏠"
/>

这里 classNames 实际是一个形如 { root: 'xxx-hash' } 的对象;如果你不想依赖 antd-style,也可以直接手写对象形式:

<Cascader
  classNames={{
    root: 'my-cascader-root',
    suffix: 'my-cascader-suffix',
    popup: { root: 'my-popup', listItem: 'my-item' },
  }}
/>

其中 popup 必须保持嵌套对象结构(root / list / listItem),组件内部会原样向下传递并拼接到真实类名中。这一点已被单元测试覆盖:components/cascader/tests/semantic.test.tsx 中同时传入 classNamesstyles,随后断言 custom-rootcustom-popup-listcustom-popup-list-item 等类所在元素确实被注入了对应样式。

3. 占位符与多选标签同样可定制

示例没有展开,但从测试可确认节点覆盖面远不止 root/prefix/suffix:

  • 占位符节点:单独测试了 classNames.placeholder / styles.placeholder(见 semantic.test.tsx);
  • 多选节点:multiple 模式下 contentitemitemContentitemRemove 全部生效(见 semantic.test.tsx),例如给标签加背景、给移除按钮调红都可以按需做:
<Cascader
  multiple
  classNames={{
    item: 'my-tag',
    itemRemove: 'my-tag-remove',
  }}
  styles={{
    item: { borderRadius: 6 },
    itemContent: { color: 'rgba(0,0,0,0.88)' },
  }}
/>

三、函数形式:依据 props 动态返回样式

当样式需要依赖组件的实时状态(如形态变体、禁用、尺寸、校验状态)时,classNames / styles 都可以传入函数。函数签名统一为 (info: { props }) => 语义节点对象,函数会拿到合并后的完整组件 props,而不仅是当前传入的 props——从 components/cascader/index.tsx#L402-L409 可以看到,源码会把 variantsizestatusdisabled 的最终合并值放进 mergedProps 后再交给合并逻辑:

const mergedProps: CascaderProps<any> = {
  ...props,
  variant,
  size: mergedSize,
  status: mergedStatus,
  disabled: mergedDisabled,
};

示例中的第二只 Cascader 即使用函数形式:当形态变体为 filled 时,把前缀、后缀乃至浮层选项条目的文字统一改成主题蓝 #1890ff;否则返回空对象,不产生任何样式覆盖:

const stylesFn: CascaderProps['styles'] = (info) => {
  if (info.props.variant === 'filled') {
    return {
      prefix: { color: '#1890ff' },
      suffix: { color: '#1890ff' },
      popup: {
        listItem: { color: '#1890ff' },
      },
    };
  }
  return {};
};

<Cascader
  options={options}
  variant="filled"
  classNames={classNames}
  styles={stylesFn}
  prefix="✅"
/>

类型说明:CascaderProps['classNames']CascaderProps['styles'] 在类型层面被定义为「对象 函数」的联合类型。其生成逻辑位于 components/_util/hooks/useMergeSemantic/semanticType.tsclassNamesAndFn = 对象 | (info) => 对象stylesAndFn 同理,因此在 TS 下传错形态会被立刻提示。运行时由 resolveStyleOrClass 判断:是函数就执行 value(info) 取结果,否则原样使用(见 useMergeSemantic/index.ts)。

函数式的禁用态样式在测试中也有覆盖:当 props.disabled 为真时返回灰底半透明、文字变灰的样式,最终断言类 .disabled-cascader 上的背景与透明度符合预期(见 semantic.test.tsx)。因此你也可以用它实现「禁用 / 普通态动态换肤」:

const classNamesFn: CascaderProps['classNames'] = ({ props }) => ({
  root: props.disabled ? 'disabled-cascader' : 'enabled-cascader',
});

四、浮层(popup)嵌套节点的专项定制与旧 API 迁移

popup 是唯一的嵌套语义节点,需要按 root / list / listItem 逐层描述。在 components/cascader/index.tsx#L414-L427 调用 useMergeSemantic 时传入的 schema 声明了 popup: { _default: 'root' },含义是:当你把 popup 写成字符串而不是对象时(例如 classNames={{ popup: 'x' }}),该字符串会被自动落到 popup.root 上,让浮层最外层拿到这个类。

源码同时提供了一条便捷的废弃映射表(index.tsx#L278-L298),在开发环境下会为使用了旧属性的代码打 deprecation 警告,提示迁移到语义化节点:

旧属性(v5 起废弃) 新写法 说明
popupClassName / dropdownClassName classNames.popup.root 浮层最外层类名
dropdownStyle styles.popup.root 浮层容器样式
dropdownMenuColumnStyle / popupMenuColumnStyle styles.popup.listItem 菜单选项列样式
dropdownRender popupRender 自定义下拉内容
onDropdownVisibleChange / onPopupVisibleChange onOpenChange 浮层显隐回调
bordered variant 边框改用形态变体表达

官方组件 API 表中同样标注了这些映射关系(见 components/cascader/index.zh-CN.md)。也就是说:如果你想改浮层的圆角、阴影或菜单项的背景,语义化时代最标准的做法是给 styles.popup.root / styles.popup.listItem 传值,而不是继续用已经废弃的 dropdownStyle

<Cascader
  styles={{
    popup: {
      root: { borderRadius: 12, boxShadow: '0 6px 16px rgba(0,0,0,0.08)' },
      listItem: { lineHeight: '32px' },
    },
  }}
/>

五、借助 ConfigProvider 做组件级全局语义配置

classNames / styles 并不仅限于单实例传入,也可以放进 ConfigProvider 的组件级配置 cascader 下作为全局默认值。在组件内部,上下文中的 contextClassNames / contextStyles 会被取出(见 index.tsx#L248-L261)并与实例 props 合并(见 index.tsx#L414-L427):

<ConfigProvider
  cascader={{
    styles: { root: { borderRadius: 8 } },
    classNames: { popup: { root: 'app-cascader-popup' } },
  }}
>
  <App />
</ConfigProvider>

合并顺序与优先级同样经过了验证:测试 should follow root style priority(见 semantic.test.tsx)同时注入 ConfigProvider 的 contextStyles/contextStyle 与实例的 styles/style,断言最终的根元素样式遵循了「实例内联 style 覆盖组件语义 styles,组件语义 styles 覆盖全局配置」的既定优先级。API 文档在 classNames/styles 两行的「全局配置」列标记为 5.25.0,即自该版本起可通过 ConfigProvider 组件级配置下发(components/cascader/index.zh-CN.md#L90)。

六、底层合并机制:useMergeSemantic 做了什么

components/_util/hooks/useMergeSemantic/index.ts 中可以清楚地看到两件事:

  1. 对象 / 函数先统一解析:把所有来源(全局 context 与实例 props)的 classNames / styles 放进数组,逐个经 resolveStyleOrClass 把函数执行掉,得到纯对象列表后再合并;
  2. 扁平节点直接拼接,嵌套节点递归合并mergeClassNames 遇到 schema 中没有声明嵌套的键,就把多个来源的字符串用 clsx 拼到一起;遇到带子结构 schema 的键(如 popup),则递归合并到 root/list/listItem 等子字段(见 useMergeSemantic/index.ts#L21-L47)。mergeStyles 则对同一节点的样式对象做浅展开,后者覆盖前者的同名属性。

所以函数形式不仅可以在多个实例间复用「基于 props 的条件样式」,还能与 ConfigProvider 的全局配置共存——函数返回对象同样会经历层级合并,最终 classNamesstyles 都被补全为覆盖所有语义节点的完整结构再下发给底层 RcCascader(见 index.tsx#L449-L499 的渲染段,classNames={mergedClassNames}styles={mergedStyles}popupStyle 亦取自合并后的 mergedStyles.popup.root)。

七、从 style-class 示例落地到你的业务

将上面知识点整合起来,一个完整的、可直接运行的双演示组件如下(取自 style-class.tsx,并保留其完整调用链):

import React from 'react';
import { Cascader, Flex } from 'antd';
import type { CascaderProps, GetProp } from 'antd';
import { createStyles } from 'antd-style';

const useStyles = createStyles(({ token }) => ({
  root: { borderRadius: token.borderRadiusLG },
}));

interface Option {
  value: string;
  label: string;
  children?: Option[];
}

const options: Option[] = [
  {
    value: 'meet-student',
    label: 'meet-student',
    children: [
      { value: 'hangzhou', label: 'Hangzhou', children: [{ value: 'xihu', label: 'West Lake' }] },
    ],
  },
  {
    value: 'jiangsu',
    label: 'Jiangsu',
    children: [
      { value: 'nanjing', label: 'Nanjing', children: [{ value: 'zhonghuamen', label: 'Zhong Hua Men' }] },
    ],
  },
];

const stylesObject: CascaderProps['styles'] = {
  prefix: { color: '#ccc' },
  suffix: { color: '#ccc' },
};

const stylesFn: CascaderProps['styles'] = (info): GetProp<CascaderProps, 'styles', 'Return'> => {
  if (info.props.variant === 'filled') {
    return {
      prefix: { color: '#1890ff' },
      suffix: { color: '#1890ff' },
      popup: { listItem: { color: '#1890ff' } },
    };
  }
  return {};
};

const App: React.FC = () => {
  const { styles: classNames } = useStyles();
  return (
    <Flex vertical gap="medium">
      <Cascader
        options={options}
        placeholder="Object styles"
        classNames={classNames}
        styles={stylesObject}
        prefix="🏠"
      />
      <Cascader
        options={options}
        placeholder="Function styles"
        variant="filled"
        classNames={classNames}
        styles={stylesFn}
        prefix="✅"
      />
    </Flex>
  );
};

export default App;

在把该模式推广到生产代码时,建议留意三点:

  • 键名严格受限classNames / styles 的键必须取自 CascaderSemanticType 中的语义节点(rootprefixsuffixinputplaceholdercontentitemitemContentitemRemove 以及嵌套的 popup.root/list/listItem),任意键会被合并逻辑当作扁平键处理,无法命中目标 DOM;
  • class 与行内样式分工:动画、媒体查询、伪类等能力行内样式无法表达,优先用 classNames + CSS(antd-style、CSS Modules 均可);简单的一次性颜色、圆角、间距可用 styles 快速落地;
  • 函数入参是「合并后」的 propsinfo.props 中拿到的是 variant/size/status/disabled 经过 ConfigProvider 与实例合并的最终值(见 index.tsx#L402-L409),因此基于状态的条件定制在单选、多选、表单内、浮层展开等各种场景下都行为一致。

小结

Cascader 的语义化定制体系由三个环节组成:语义节点类型定义CascaderSemanticType)保证键名可校验、useMergeSemantic 负责把 ConfigProvider 全局配置与实例的对象 / 函数统一合并、popup 嵌套 schema 让浮层内部结构也能逐层寻址。配合 style-class 示例semantic.test.tsx 中对象形式、函数形式、多选节点、占位符节点、root 优先级五类测试用例,你可以放心地用 classNames / styles 替代历史遗留的 dropdownClassNamedropdownStyledropdownMenuColumnStyle 等 API,以稳定的语义化路径完成从单实例到全局的精细样式定制。

登录后查看全文
热门项目推荐
相关项目推荐