Foundational Concepts for Creating Custom Components on Top of Ant Design Primitives

Ant Design enables custom component creation through a token-driven, CSS-in-JS architecture that separates style generation from component logic, allowing extensions to inherit global themes, ConfigProvider defaults, and semantic styling capabilities.

Creating custom components that feel native to Ant Design requires understanding its internal architectural layers. The ant-design/ant-design repository exposes a sophisticated system of design tokens, context providers, and semantic merging utilities that allow third-party components to integrate seamlessly with the design system. Mastering these foundational concepts ensures your custom components automatically respect theme changes, directionality, and global configuration overrides just like core Ant Design primitives.

Token-Driven CSS-in-JS Architecture

Ant Design's styling system revolves around design tokens and runtime CSS generation. When building custom components, you interact with the theme layer through specific hooks and registration functions defined in the core theme system.

Theme Tokens and Style Registration

The entry point for component styling is components/theme/internal.ts, which exports genStyleHooks and useStyleRegister. These utilities generate scoped CSS at runtime (or build-time when using zero-runtime mode) and return hash identifiers for scoping.

A custom component typically invokes genStyleHooks with the component name and a style generator function:

import { genStyleHooks } from '../../theme/internal';
import { prepareToken } from './token';

export default genStyleHooks(
  'Button',
  (token) => {
    const buttonToken = prepareToken(token);
    return [ /* style generators */ ];
  },
  prepareComponentToken,
);

This pattern returns a tuple of [hashId, cssVarCls] that your component applies to the root element. The hashId ensures style scoping, while cssVarCls handles CSS variable wrapping.

Component Token Preparation

For components requiring custom token values, define a prepareToken or prepareComponentToken function that extends the global token interface. As implemented in components/button/style/token.ts, this function derives component-specific values from the global theme:

export interface ButtonToken {
  fancyBorderRadius?: string;
}

export const prepareToken = (token: GlobalToken): ButtonToken => ({
  ...token,
  fancyBorderRadius: token.controlHeight,
});

These prepared tokens become available through the theme system and can be accessed via ConfigProvider theme overrides.

ConfigProvider Context and Global Defaults

The ConfigProvider system supplies global configuration including prefix classes, direction, locale, and component-level default props. Custom components consume this context through the useComponentConfig hook exported from components/config-provider/context.ts.

import { useComponentConfig } from '../config-provider/context';

const {
  getPrefixCls,
  direction,
  classNames: contextClassNames,
  styles: contextStyles,
} = useComponentConfig('button');

The useComponentConfig hook accepts a component key (e.g., 'button') and returns the global defaults that users may have configured via <ConfigProvider component={{ button: { ... } }}>. This ensures your custom component respects global classNames, styles, and variant settings.

Semantic ClassNames and Styles Merging

Ant Design 5.x introduces a semantic API allowing users to pass nested classNames and styles objects targeting specific component parts (root, header, body, etc.). Custom components must merge these with context defaults using useMergeSemantic from components/_util/hooks/useMergeSemantic.ts.

const [mergedClassNames, mergedStyles] = useMergeSemantic<
  ButtonClassNamesType,
  ButtonStylesType,
  ButtonProps
>([contextClassNames, classNames],
  [contextStyles, styles],
  { props: mergedProps });

This utility resolves function-valued semantic definitions and performs deep merging according to Ant Design's nested schema. It ensures that user-provided overrides layer correctly over global configuration without losing structural semantics.

The Six-Step Implementation Pattern

When constructing a custom component on Ant Design primitives, follow this architectural contract:

  1. Declare token interfaces and preparation functions if extending the theme system with new visual parameters.

  2. Create a style hook using genStyleHooks or genComponentStyleHook that returns [hashId, cssVarCls] for CSS scoping.

  3. Consume ConfigProvider via useComponentConfig to obtain prefix classes, direction, and global default props.

  4. Merge semantic data with useMergeSemantic to handle classNames and styles props that conform to Ant Design's semantic schema.

  5. Compose final JSX using merged classNames, styles, the derived prefixCls, and forwarded refs.

  6. Export with forwardRef to maintain API compatibility with native Ant Design components.

Complete Example: Building a FancyButton Component

Below is a fully functional custom component that extends Ant Design's Button primitive while maintaining full compatibility with the theme system and ConfigProvider.

// src/components/fancy-button/FancyButton.tsx
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { useComponentConfig } from '../../config-provider/context';
import useStyle from '../../button/style';
import { useMergeSemantic } from '../../_util/hooks';
import type { ButtonProps } from '../../button/button';

const useFancyStyle = (prefixCls: string) => {
  const [hashId, cssVarCls] = useStyle(prefixCls);
  return [hashId, cssVarCls] as const;
};

const FancyButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
  const {
    className,
    style,
    classNames,
    styles,
    prefixCls: customPrefix,
    ...rest
  } = props;

  const {
    getPrefixCls,
    direction,
    classNames: ctxClassNames,
    styles: ctxStyles,
  } = useComponentConfig('button');

  const prefixCls = getPrefixCls('btn', customPrefix);
  const [hashId, cssVarCls] = useFancyStyle(prefixCls);

  const [mergedClassNames, mergedStyles] = useMergeSemantic<
    any,
    any,
    ButtonProps
  >([ctxClassNames, classNames], [ctxStyles, styles], { props });

  const classes = clsx(
    prefixCls,
    hashId,
    cssVarCls,
    className,
    mergedClassNames?.root,
    `fancy-${direction}`,
  );

  const finalStyle = {
    ...mergedStyles?.root,
    ...style,
    borderRadius: 'var(--fancy-border-radius, 8px)',
  };

  return (
    <button
      {...rest}
      ref={ref}
      className={classes}
      style={finalStyle}
    />
  );
});

export default FancyButton;

Key implementation details:

  • Reuses useStyle from the original Button component to maintain consistent CSS-in-JS registration and hash scoping.
  • Applies useMergeSemantic to combine context defaults with user-provided semantic props.
  • Derives prefixCls via getPrefixCls('btn', customPrefix) to respect global prefix configurations.
  • Handles directionality through the direction value from ConfigProvider context.

Summary

Frequently Asked Questions

How do I access global theme tokens in a custom component?

Import useToken from components/theme/internal.ts to access the global token object within your component body. This hook returns the complete theme including color palettes, spacing scales, and component-specific overrides configured via ConfigProvider.

What is the purpose of useMergeSemantic in Ant Design?

The useMergeSemantic hook resolves and deep-merges semantic classNames and styles props according to Ant Design's nested schema. It handles both static objects and function-valued definitions, ensuring that user overrides layer correctly over global ConfigProvider defaults while maintaining type safety.

How do I register custom design tokens that work with ConfigProvider?

Define a prepareComponentToken function that returns an interface extending the global token type. Pass this function as the third argument to genStyleHooks. The returned token values become accessible within your style generators and can be overridden via the theme prop on ConfigProvider.

Can I reuse existing Ant Design style hooks in my custom components?

Yes. Import and invoke the useStyle hook exported by existing components (such as components/button/style) to inherit their CSS-in-JS registration logic. This approach ensures your custom component receives the same hash scoping and CSS variable treatment as the primitive component it extends.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →