CoreUI is in beta - APIs and components may change.
CoreUI
Utils

Polymorphic

Utility types for strongly typed polymorphic components.


Types for components that render a different element depending on an as prop. CoreUI uses them for icon slots, where asChild would mean cloning an element just to swap a tag:

<FancyButton.Icon as={RiArrowRightLine} />

The types give as autocomplete and then type the remaining props against whatever component you passed. They're only needed if you use TypeScript.

Installation

Create a utils/polymorphic.ts file and paste the following code into it.

utils/polymorphic.ts
type AsProp<T extends React.ElementType> = {  as?: T;};type PropsToOmit<T extends React.ElementType, P> = keyof (AsProp<T> & P);type PolymorphicComponentProp<  T extends React.ElementType,  Props = {},> = React.PropsWithChildren<Props & AsProp<T>> &  Omit<React.ComponentPropsWithoutRef<T>, PropsToOmit<T, Props>>;export type PolymorphicRef<T extends React.ElementType> =  React.ComponentPropsWithRef<T>['ref'];type PolymorphicComponentPropWithRef<  T extends React.ElementType,  Props = {},> = PolymorphicComponentProp<T, Props> & { ref?: PolymorphicRef<T> };export type PolymorphicComponentPropsWithRef<  T extends React.ElementType,  P = {},> = PolymorphicComponentPropWithRef<T, P>;export type PolymorphicComponentProps<  T extends React.ElementType,  P = {},> = PolymorphicComponentProp<T, P>;export type PolymorphicComponent<P> = {  <T extends React.ElementType>(    props: PolymorphicComponentPropsWithRef<T, P>,  ): React.ReactNode;};

Examples

Without forwardRef

PolymorphicComponentProps<T, OwnProps> merges your own props with the props of T, minus the ones you've already claimed. This is how every CoreUI icon slot is written — here is the real one from fancy-button.tsx:

components/ui/fancy-button.tsx
import { PolymorphicComponentProps } from '@/utils/polymorphic';

function FancyButtonIcon<T extends React.ElementType>({
  className,
  variant,
  size,
  as,
  ...rest
}: PolymorphicComponentProps<T, FancyButtonSharedProps>) {
  const Component = as || 'div';
  const { icon } = fancyButtonVariants({ variant, size });

  return <Component className={icon({ class: className })} {...rest} />;
}

Because as defaults to div, both of these type-check, and the second one accepts every prop a Remix icon takes:

<FancyButton.Icon />
<FancyButton.Icon as={RiArrowRightLine} />

With forwardRef

No CoreUI component needs this, since ref is a plain prop in React 19. If you're supporting an older major, the generic has to be threaded through by hand:

example.tsx
import * as React from 'react';

import { cn } from '@/utils/cn';
import {
  PolymorphicComponent,
  PolymorphicComponentPropsWithRef,
  PolymorphicRef,
} from '@/utils/polymorphic';

type ButtonOwnProps = {
  size?: 'large' | 'medium';
};

const Button: PolymorphicComponent<ButtonOwnProps> = React.forwardRef(
  <T extends React.ElementType = 'button'>(
    {
      as,
      className,
      size = 'large',
      ...rest
    }: PolymorphicComponentPropsWithRef<T, ButtonOwnProps>,
    forwardedRef?: PolymorphicRef<T>,
  ) => {
    const Component = as || 'button';

    return (
      <Component
        ref={forwardedRef}
        className={cn(
          {
            'h-12 px-4': size === 'large',
            'h-9 px-3': size === 'medium',
          },
          className,
        )}
        {...rest}
      />
    );
  },
);

API Reference

TypeUse
PolymorphicComponentProps<T, P>Props for a polymorphic component without a ref
PolymorphicComponentPropsWithRef<T, P>The same, plus a correctly typed ref
PolymorphicRef<T>The ref type of T, for a forwardRef parameter
PolymorphicComponent<P>Annotates the component itself so the generic survives forwardRef

On this page