Polymorphic components

What is a polymorphic component

A polymorphic component is a component which root element can be changed with component prop. All polymorphic components have a default element which is used when component prop is not provided. For example, the Button component default element is button and it can be changed to a or any other element or component:

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

function Demo() {
  return (
    <Button component="a" href="https://mantine.dev/" target="_blank">
      Mantine website
    </Button>
  );
}

renderRoot prop

renderRoot is an alternative to the component prop, which accepts a function that should return a React element. It is useful in cases when component prop cannot be used, for example, when the component that you want to pass to the component is generic (accepts type or infers it from props, for example <Link<'/'> />).

Example of using renderRoot prop, the result is the same as in the previous demo:

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

function Demo() {
  return (
    <Button
      renderRoot={(props) => (
        <a href="https://mantine.dev/" target="_blank" {...props} />
      )}
    >
      Mantine website
    </Button>
  );
}

!important It is required to spread props argument into the root element. Otherwise there will be no styles and the component might not be accessible.

Polymorphic components as other React components

You can pass any other React component to component prop. For example, you can pass Link component from react-router-dom:

import { Link } from 'react-router-dom';
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Button component={Link} to="/react-router">
      React router link
    </Button>
  );
}

Polymorphic components as Next.js Link

Next.js link does not work in the same way as other similar components in all Next.js versions.

With Next.js 12 and below:

import Link from 'next/link';
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Link href="/hello" passHref>
      <Button component="a">Next link button</Button>
    </Link>
  );
}

With Next.js 13 and above:

import Link from 'next/link';
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Button component={Link} href="/hello">
      Next link button
    </Button>
  );
}

Polymorphic components with generic components

You cannot pass generic components to component prop because it is not possible to infer generic types from the component prop. For example, you cannot pass typed Next.js Link to component prop because it is not possible to infer href type from the component prop. The component itself will work correctly, but you will have a TypeScript error.

To make generic components work with polymorphic components, use renderRoot prop instead of component:

import Link from 'next/link';
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Button renderRoot={(props) => <Link href="/hello" {...props} />}>
      Typed Next link button
    </Button>
  );
}

Polymorphic components with react-router NavLink

react-router-dom NavLink component className prop accepts a function based on which you can add an active class to the link. This feature is incompatible with Mantine component prop, but you can use renderRoot prop instead:

import cx from 'clsx';
import { NavLink } from 'react-router-dom';
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Button
      renderRoot={({ className, ...others }) => (
        <NavLink
          className={({ isActive }) =>
            cx(className, { 'active-class': isActive })
          }
          {...others}
        />
      )}
    >
      React router NavLink
    </Button>
  );
}

Wrapping polymorphic components

Non-polymorphic components include React.ComponentPropsWithoutRef<'x'> as a part of their props type, where x is a root element of the component. For example, Container component is not polymorphic – its root element is always div, so its props type includes React.ComponentPropsWithoutRef<'div'>.

Polymorphic components do not include React.ComponentPropsWithoutRef<'x'> as a part of their props type because their root element can be changed, and thus props type can be inferred only after the component was rendered.

Example of creating a non-polymorphic wrapper component for Mantine polymorphic component:

import { forwardRef } from 'react';
import { Button, ButtonProps } from '@mantine/core';

interface LinkButtonProps
  extends ButtonProps,
    Omit<React.ComponentPropsWithoutRef<'a'>, keyof ButtonProps> {}

const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>((props, ref) => (
  <Button {...props} ref={ref} component="a" />
));

function Demo() {
  return (
    <LinkButton href="https://mantine.dev" target="_blank">
      Mantine website
    </LinkButton>
  );
}

Example of creating a polymorphic wrapper component for Mantine polymorphic component:

Anchor by default
import { forwardRef } from 'react';
import { createPolymorphicComponent, Button, ButtonProps, Group } from '@mantine/core';

interface CustomButtonProps extends ButtonProps {
  label: string;
}

// Default root element is 'button', but it can be changed with 'component' prop
const CustomButton = createPolymorphicComponent<'button', CustomButtonProps>(
  forwardRef<HTMLButtonElement, CustomButtonProps>(({ label, ...others }, ref) => (
    <Button {...others} ref={ref}>
      {label}
    </Button>
  ))
);

// Default root element is 'a', but it can be changed with 'component' prop
const CustomButtonAnchor = createPolymorphicComponent<'a', CustomButtonProps>(
  forwardRef<HTMLAnchorElement, CustomButtonProps>(({ label, ...others }, ref) => (
    <Button component="a" {...others} ref={ref}>
      {label}
    </Button>
  ))
);

function Demo() {
  return (
    <Group>
      <CustomButton label="Button by default" color="cyan" />
      <CustomButtonAnchor label="Anchor by default" href="https://mantine.dev" target="_blank" />
    </Group>
  );
}

Dynamic component prop

You can use dynamic value in the component prop, but in this case, you need to either provide types manually or disable type checking by passing any as a type argument to the polymorphic component:

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

function KeepTypes() {
  return (
    <Box<'input'>
      component={(Math.random() > 0.5 ? 'input' : 'div') as any}
    />
  );
}

function NukeTypes() {
  return (
    <Box<any> component={Math.random() > 0.5 ? 'input' : 'div'} />
  );
}

Create your own polymorphic components

Use createPolymorphicComponent function and Box component to create new polymorphic components:

MyButton as anchor
import { forwardRef } from 'react';
import { Box, BoxProps, createPolymorphicComponent, Group } from '@mantine/core';

interface MyButtonProps extends BoxProps {
  label: string;
}

const MyButton = createPolymorphicComponent<'button', MyButtonProps>(
  forwardRef<HTMLButtonElement, MyButtonProps>(({ label, ...others }, ref) => (
    <Box component="button" {...others} ref={ref}>
      {label}
    </Box>
  ))
);

function Demo() {
  return (
    <Group>
      <MyButton label="Button by default" />
      <MyButton
        label="MyButton as anchor"
        component="a"
        href="https://mantine.dev"
        target="_blank"
      />
    </Group>
  );
}

Make Mantine component polymorphic

Polymorphic components have performance overhead for tsserver (no impact on runtime performance), because of that not all Mantine components have polymorphic types, but all components still accept component prop – root element can be changed.

To make Mantine component polymorphic, use createPolymorphicComponent function the same way as in the previous example:

import {
  createPolymorphicComponent,
  Group,
  GroupProps,
} from '@mantine/core';

const PolymorphicGroup = createPolymorphicComponent<
  'button',
  GroupProps
>(Group);

function Demo() {
  return (
    <PolymorphicGroup component="a" href="https://mantine.dev" />
  );
}