npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-polymorphic

v1.0.3

Published

Create polymorphic React 19+ components with strong TypeScript inference.

Readme

react-polymorphic

CI npm version license

Great to Perfect. The catalyst for your design-system.

A single component factory function for polymorphic React components. Built for React 19+ with strong TypeScript inference.

import { createPolymorph } from 'react-polymorphic';

const Button = createPolymorph<'button'>(({ as: Tag = 'button', ...rest }) => {
  return <Tag {...rest} />
})

const App = () => {
  return <Button as="a" href="/">Home</Button>
}

Table of Contents

Why Polymorphic Components?

Design systems need components that are reusable without being rigid.
A <Button /> might need to render as a native <button /> in one context and as an <a /> in another. A heading component may need to shift from <h1 /> to <h3 /> based on document hierarchy.

Polymorphic components let you keep one consistent API while swapping the rendered element to match context (button -> a, div -> section, h1 -> h3), preserving semantics, accessibility, and developer ergonomics.

react-polymorphic is built to take this beyond a basic as prop, with an API that gives you precise control over inherited props, component composition, and render behavior.

Features

  • Polymorphism Beyond as
    Supports intrinsic elements, React components, and composed polymorphic components.

  • root Override for Composition
    Control the final rendered element when nesting polymorphic components.

  • Type-safe Inherited Prop Control
    Use PolymorphicConfig with Inherit and Unset to preserve, remove, or reshape inherited props.

  • Ref Inference that Follows Render Target
    ref types track the active element/component target.

  • Inference Utilities Included
    InferPolymorphElement, InferPolymorphProps, and InferPolymorphConfig for advanced typing workflows.

  • TypeScript 7 Ready
    Compatible with TypeScript 5.4+ and optimized for TypeScript 7.

  • Built for React 19+
    Leverages React 19's handling of ref, eliminating forwardRef and convoluted type gymnastics.

  • ESM Exclusive
    No CommonJS, no legacy build artifacts. Tree-shakable and optimized for modern bundlers.

Installation

npm install react-polymorphic

Use the equivalent command for your package manager of choice.

Examples

1 — Basic Usage

Most polymorphic components are simple wrappers that combine custom props with inferred DOM attributes.

import { createPolymorph } from 'react-polymorphic';

type Props = {
  variant: 'primary' | 'secondary';
}

const Pane = createPolymorph<'div', Props>((props) => {
  const { as: Tag = 'div', variant, className, ...rest } = props;
  const variantClasses = variant === 'primary' ? 'bg-white' : 'bg-blue-50';

  return <Tag className={`p-4 ${variantClasses} ${className}`} {...rest} />
})

const App = () => {
  return (
    <>
      <Pane as="section" variant="primary" aria-label="...">...</Pane>
      <Pane as="aside" variant="secondary" aria-label="...">...</Pane>
    </>
  )
}

Props and the intrinsic attributes of 'div' are merged into one contract. Consumers can then switch semantics with as (section, aside, etc.) while keeping the same component API.

2 — Using ref

ref is inferred from the active render target. With React 19, you can use ref directly without forwardRef.

import { useRef } from 'react';
import { createPolymorph } from 'react-polymorphic';

const Component = createPolymorph<'div'>((props) => {
  const { as: Tag = 'div', ...rest } = props;
  return <Tag {...rest} />
})

const App = () => {
  const ref = useRef<HTMLDivElement>(null);
  const aRef = useRef<HTMLAnchorElement>(null);

  return (
    <>
      <Component ref={ref} />
      <Component as="a" ref={aRef} />
    </>
  )
}

ref follows as automatically (and also follows root when composing with polymorphic components).

3 — Config & Branded Types

Use PolymorphicConfig to reshape inferred props: remove, preserve, override, or require attributes.

import { createPolymorph } from 'react-polymorphic';
import type { PolymorphicConfig, Unset, Inherit } from 'react-polymorphic';

type Props = {
  loading: boolean;
}

type Config = PolymorphicConfig<'button', {
  'aria-loading': Unset;
  children: string;
  href: string;
  target?: '_blank';
  type: Inherit;
}>;

const Button = createPolymorph<'button', Props, Config>((props) => {
  const { as: Tag = 'button', children, loading, ...rest } = props;

  return (
    <Tag aria-loading={loading} {...rest}>
      {loading ? 'Loading...' : children}
    </Tag>
  )
})

const App = () => {
  return (
    <>
      <Button type="button">Calculate</Button>
      <Button as="a" href="/">Back to home</Button>
    </>
  )
}
  • Unset removes an inferred prop from the resulting contract.
  • Inherit preserves the inferred type (useful when making a prop required or composing unions).
  • Adding href and target to Config prepares the component for common as="a" usage.
  • PolymorphicConfig is optional, but recommended for code hints and safer attribute authoring.

4 — Advanced as Composition with root

When as points to another polymorphic component, root lets you control that component's final underlying element.

import { createPolymorph } from 'react-polymorphic';

const OtherComponent = createPolymorph<'div'>(...);

const Component = createPolymorph<'div'>((props) => {
  const { as: Tag = 'div', ...rest } = props;
  return <Tag {...rest} />
});

const App = () => {
  return <Component as={OtherComponent} root="section" />
}

root behaves similarly to key: it influences rendering behavior but is not part of the consumed props inside your render function.

5 — Advanced Composition & Inference

You can extract generics from existing polymorphic components and reuse them when extending third-party components.

import { createPolymorph } from 'react-polymorphic';
import type {
  InferPolymorphElement,
  InferPolymorphProps,
  InferPolymorphConfig
} from 'react-polymorphic';
import { TheirButton } from 'some-library';

type TheirElement = InferPolymorphElement<typeof TheirButton>;
// 'button'

type TheirProps = InferPolymorphProps<typeof TheirButton>;
// { message: string }

type TheirConfig = InferPolymorphConfig<typeof TheirButton>;
// { id?: number; title: string }

type Props = { ... }

const OurButton = createPolymorph<typeof TheirButton, Props>((props) => {
  const { as: Tag = TheirButton, ...rest } = props;
  return <Tag {...rest} />
})

This pattern keeps your extension aligned with the base component contract while still allowing consumers to swap as.

If swapping out the base would break core behavior, preserve the base and expose only its internal polymorphism:

const OurButton = createPolymorph<TheirElement, Props & TheirProps, TheirConfig>(
  (props) => {
    const { as = 'button', ...rest } = props;
    return <TheirButton as={as} {...rest} />
  }
)

const App = () => {
  return <OurButton as="a" />
}

In this version, OurButton always renders TheirButton, and as only controls what TheirButton itself renders to the DOM.

API

createPolymorph

Creates polymorphic components with strong inference for as, root, and ref.

| Export | Kind | | ----------------- | ---------- | | createPolymorph | function |

Component Props

| Prop | Type | Notes | | ------ | ---------------------------------- | ------------------------ | | as? | element | component | Swaps target. | | root? | element | Fragment | Overrides nested target. | | ref? | inferred from final target | Follows as/root. |

Generics

| Generic | Meaning | | ------- | ------------------------------------ | | T | Default render target. | | P | Custom component props. | | C | Prop config via PolymorphicConfig. |

Utility Types

| Export | Purpose | | -------------------------- | ------------------------------ | | PolymorphicConfig<T, C> | Configures inherited props. | | Inherit | Preserves inferred prop type. | | Unset | Removes an inferred prop. | | InferPolymorphElement<T> | Extracts default target type. | | InferPolymorphProps<T> | Extracts custom props type. | | InferPolymorphConfig<T> | Extracts config type. |

Compatibility

| Package | Version | | ------------ | ---------- | | react | >=19.0.0 | | react-dom | >=19.0.0 | | typescript | >=5.4.0 |

Contributing + Changesets

Contributions are welcome. For bug fixes, improvements, or new features, please open a PR with a clear summary of the change.

Local setup

pnpm install
pnpm typecheck
pnpm test:run

PR requirements

  • Keep changes focused and include tests when behavior changes.
  • Add a changeset for all user-facing package changes:
pnpm changeset

When prompted, choose the correct bump type:

  • patch → fixes and small improvements
  • minor → new backward-compatible features
  • major → breaking changes

If a PR does not require a release, you can create an empty changeset:

pnpm changeset --empty

Automation

  • Pre-commit runs biome check --write on staged files.
  • Pre-push runs pnpm typecheck and pnpm test:run.
  • CI validates formatting, linting, type-checking, tests, and build.
  • Release runs only after CI succeeds on main, using Changesets to version and publish.

License

Apache-2.0. See LICENSE and NOTICE.