CodingCode ReviewIntermediate30 minSaves 30 minutes

React Polymorphic Button Component with `as` Prop

For React engineers building design systems, generate a robust, type-safe polymorphic Button component that correctly infers props for `as` prop values like `button`, `a`, or `Link`.

Develop a React polymorphic Button component enabling `as` prop usage for `button`, `a`, or `Link` elements. This solution ensures strict type safety by inferring correct props for each element, crucial for maintaining consistency and reducing errors in shared design systems.

READY-TO-USE PROMPT

Copy Prompt

prompt.txt
Role:
Assume the role of a senior React developer and TypeScript expert. You are responsible for architecting core components within a large-scale design system. Your focus is on creating highly reusable, performant, and type-safe primitives that other developers can confidently use.

Context:
Our team is developing a new design system, and one of the foundational components is a `Button` primitive. This button needs to be highly flexible, capable of rendering as a standard HTML `button`, an `<a>` tag, or even a React Router `Link` component, all while maintaining strict type safety. The primary challenge is ensuring that when the `as` prop changes, the component correctly infers and accepts the native props for the rendered element (e.g., `href` for `<a>`, `to` for `Link`, `disabled` for `button`). This polymorphic behavior is critical for reducing duplication and ensuring consistency across our application suite.

Task:
Your task is to implement a robust, type-safe polymorphic `Button` component using TypeScript (TSX).
1.  **Polymorphism**: The component must accept an `as` prop.
    *   When `as` is omitted or `button`, it should render a standard `<button>` element.
    *   When `as` is `a`, it should render an `<a>` element.
    *   When `as` is a React Router `Link` component (e.g., `as={Link}`), it should render that component.
2.  **Type Safety & Inference**:
    *   The component's props must correctly infer based on the value of the `as` prop. For example, if `as="a"`, `href` should be a valid prop, but `to` should not. If `as={Link}`, `to` should be valid, but `href` should not.
    *   Ensure all native HTML attributes for the respective element are correctly typed and accepted (e.g., `onClick`, `type`, `target`, `rel`, `disabled`).
3.  **Styling & Variants**:
    *   Include a `variant` prop with options: `primary`, `secondary`, `ghost`. The default should be `{{default_variant}}`.
    *   Include a `size` prop with options: `sm`, `md`, `lg`. The default should be `{{default_size}}`.
    *   The styling should be applied via a simple CSS class system or inline styles for demonstration purposes. Avoid complex styling libraries unless specifically required for clarity.
4.  **Component Structure**:
    *   Provide the full TSX code for the `Button` component, including all necessary type definitions (e.g., `PolymorphicComponentProps`, `ButtonProps`).
    *   Demonstrate clear separation of types and component logic.
5.  **Supporting Elements**:
    *   Generate a small test suite using Jest and React Testing Library that covers rendering different `as` prop values and basic prop passing.
    *   Include a section on accessibility considerations specific to a polymorphic button (e.g., `aria-label`, `role` attributes, focus management).
    *   Provide notes on performance considerations for such a component, especially regarding re-renders and prop spreading.

Constraints:
*   The implementation must use TypeScript exclusively.
*   Focus on a clean, readable, and extensible component API.
*   Assume React Router v6 for `Link` component compatibility. The `Link` component should be imported from `react-router-dom`.
*   Prioritize robust type inference and safety over minimal code size if there's a trade-off.
*   Avoid using external UI libraries or complex state management. Keep it to core React and TypeScript.
*   The `children` prop should be of type `React.ReactNode`.
*   The component should correctly forward `ref`s to the underlying element.

Output:
1.  **File Tree Structure**: A proposed file structure for the component, types, and tests.
2.  **`Button.tsx`**: Complete TypeScript React component code.
3.  **`Button.types.ts`**: Separate file for all type definitions.
4.  **`Button.test.tsx`**: Jest/React Testing Library tests.
5.  **Accessibility Notes**: Detailed considerations for ARIA attributes, keyboard navigation, and semantic HTML for the polymorphic button.
6.  **Performance Notes**: Analysis of potential performance impacts (e.g., prop spreading, re-renders) and mitigation strategies.
7.  **Example Usage**: Clear examples demonstrating how to use the `Button` with `as="button"`, `as="a"`, and `as={Link}".

Estimated results

DifficultyIntermediate
Setup time30 min
Time saved30 minutes
Best modelsClaude, ChatGPT, Gemini
Best audienceSoftware Development, Web Development

Editor's note

Why this prompt matters

Developing a comprehensive design system often presents the challenge of creating foundational components that are both versatile and strictly type-safe. The Button component is a common example where flexibility is paramount. Engineers frequently need a button that can function as a standard HTML button, an anchor link, or even a routing component like React Router's Link, all while correctly inferring and validating the specific props for each underlying element.

This workflow addresses the complexities of building such a polymorphic Button. It's designed for React engineers tasked with architecting core UI primitives, ensuring that a single component can adapt its rendered HTML element or React component type based on an as prop. The goal is to eliminate the need for separate Button, LinkButton, and AnchorButton components, thereby reducing code duplication and simplifying maintenance across a large codebase.

Reaching for this approach is appropriate when consistency in styling and behavior is critical across different interactive elements, but the underlying semantic HTML or component type needs to vary. It ensures that developers consume a unified API, benefiting from compile-time type checks that prevent common errors associated with mismatched props, such as passing an href to a Link component or a to prop to an <a> tag.

Anatomy

Prompt engineering breakdown

Role

Senior React developer and TypeScript expert building core design system components.

Context

Developing a design system's foundational `Button` primitive that requires polymorphic behavior (button, a, Link) with strict type safety and correct prop inference based on the `as` prop.

Goal

Implement a robust, type-safe polymorphic `Button` component in TSX, including styling variants, tests, accessibility, and performance notes.

Constraints

TypeScript-exclusive, clean API, React Router v6, prioritize type safety, no external UI libs/state management, `React.ReactNode` children, `ref` forwarding.

Output format

File Tree Structure, `Button.tsx`, `Button.types.ts`, `Button.test.tsx`, Accessibility Notes, Performance Notes, Example Usage.

Why this structure works

This prompt uses role priming to establish the necessary expertise and context for the task. Explicit constraints guide the model on specific implementation details and limitations, such as using TypeScript exclusively and avoiding external libraries. The detailed task breakdown and structured output format ensure a comprehensive and organized response, covering component implementation, testing, accessibility, and performance considerations.

Pick your version

Prompt variations

BeginnerWorks with any model

For learning the basics of polymorphic components or quick prototypes where extensive details are not yet needed.

prompt.txt
As a React developer, create a basic polymorphic `Button` component in TSX. It should accept an `as` prop to render as a `button`, `a`, or a `Link` (from `react-router-dom`). Ensure type safety, so `href` is available for `a` and `to` for `Link` but not vice-versa. Include `variant` (options: `primary`, `secondary`, `ghost`, default: `{{default_variant}}`) and `size` (options: `sm`, `md`, `lg`, default: `{{default_size}}`) props for basic styling. Provide the TSX code for the component and its types, along with simple usage examples.
ProfessionalBest with claude

For developing production-ready components within an established design system, requiring comprehensive solutions.

prompt.txt
Assume the role of a senior React architect. Develop a production-grade, type-safe polymorphic `Button` component for a design system. The component must support rendering as a native `button`, an `<a>` tag, or a `react-router-dom` `Link` via an `as` prop. Crucially, props must infer correctly for each element, ensuring strict TypeScript safety for attributes like `href`, `to`, `disabled`, and `onClick`. Implement `variant` (options: `primary`, `secondary`, `ghost`, default: `{{default_variant}}`) and `size` (options: `sm`, `md`, `lg`, default: `{{default_size}}`) props, applied via CSS classes. Provide the full TSX, separate type definitions, a Jest/RTL test suite, detailed accessibility considerations, and performance notes.
Short VersionWorks with any model

When you need a concise code snippet for the core polymorphic button functionality without extensive documentation.

prompt.txt
Implement a type-safe polymorphic `Button` component in TSX that accepts an `as` prop to render as `button`, `a`, or `react-router-dom` `Link`, with correct prop inference for `href` or `to`. Include `variant` (`primary`, `secondary`, `ghost`, default: `{{default_variant}}`) and `size` (`sm`, `md`, `lg`, default: `{{default_size}}`) props for styling. Provide the component code and type definitions. The component should also forward `ref`s to the underlying element.
EnterpriseBest with chatgpt

For large organizations where component quality, governance, long-term maintainability, compliance, and cross-team adoption are paramount.

prompt.txt
As a lead architect for a large enterprise design system, develop a highly resilient and type-safe polymorphic `Button` component. This component must rigorously support rendering as a `button`, `<a>` element, or `react-router-dom` `Link` via an `as` prop, with fully inferred and strictly enforced TypeScript types for all native and custom attributes. Prioritize long-term maintainability, accessibility compliance (WCAG 2.1 AA), and robust error handling. Implement `variant` (`primary`, `secondary`, `ghost`, default: `{{default_variant}}`) and `size` (`sm`, `md`, `lg`, default: `{{default_size}}`) props, ensuring clear extensibility. The output must include comprehensive TSX, dedicated type definitions, a full test suite with high coverage, detailed accessibility audits, and performance optimization strategies crucial for large-scale applications, alongside clear documentation for cross-functional teams and future governance.

What you'll get

Expected output

// Button.types.ts import type { ComponentPropsWithoutRef, ElementType } from 'react'; import type { LinkProps } from 'react-router-dom';

export type ButtonVariant = 'primary' | 'secondary' | 'ghost'; export type ButtonSize = 'sm' | 'md' | 'lg';

interface BaseButtonProps { children: React.ReactNode; variant?: ButtonVariant; size?: ButtonSize; className?: string; }

type PolymorphicComponentProps<T extends ElementType, P> = P & Omit<ComponentPropsWithoutRef<T>, keyof P>;

type ButtonAsButtonProps = PolymorphicComponentProps<'button', BaseButtonProps>; type ButtonAsAnchorProps = PolymorphicComponentProps<'a', BaseButtonProps>; type ButtonAsLinkProps = PolymorphicComponentProps<typeof Link, BaseButtonProps>;

export type ButtonProps<T extends ElementType> = T extends 'button' ? ButtonAsButtonProps : T extends 'a' ? ButtonAsAnchorProps : T extends typeof Link ? ButtonAsLinkProps : PolymorphicComponentProps<'button', BaseButtonProps>;

// Button.tsx import React, { forwardRef } from 'react'; import { Link } from 'react-router-dom'; import type { ElementType } from 'react'; import type { ButtonProps, ButtonVariant, ButtonSize } from './Button.types';

const DEFAULT_VARIANT: ButtonVariant = 'primary'; const DEFAULT_SIZE: ButtonSize = 'md';

const getClasses = (variant: ButtonVariant, size: ButtonSize, className?: string) => { const base = 'inline-flex items-center justify-center font-medium rounded-md transition-colors'; const variants = { primary: 'bg-blue-600 text-white', secondary: 'bg-gray-200 text-gray-800', ghost: 'bg-transparent text-blue-600', }; const sizes = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-5 py-2.5 text-lg', }; return [base, variants[variant], sizes[size], className].filter(Boolean).join(' '); };

export const Button = forwardRef( <T extends ElementType = 'button'>( { as, variant = DEFAULT_VARIANT, size = DEFAULT_SIZE, className, children, ...rest }: ButtonProps<T> & { as?: T }, ref: React.Ref<ElementType> ) => { const Component = as || 'button'; const classes = getClasses(variant, size, className);

return ( <Component ref={ref as any} className={classes} {...rest}> {children} </Component> ); } );

Button.displayName = 'Button';

// Example Usage Snippets // (Assuming Button.tsx and Button.types.ts are in place, and react-router-dom is installed)

// 1. Default button (renders as <button>) <Button onClick={() => alert('Hello!')}> Click Me </Button>

// 2. Button rendering as an <a> tag <Button as="a" href="https://example.com" target="_blank" rel="noopener noreferrer" variant="secondary"> External Link </Button>

// 3. Button rendering as a React Router Link import { Link } from 'react-router-dom'; <Button as={Link} to="/dashboard" variant="primary" size="lg"> Go to Dashboard </Button>

// 4. Disabled button <Button disabled variant="ghost"> Disabled Action </Button>

Under the hood

Why this prompt works

This workflow produces a robust polymorphic button by employing several key prompt engineering techniques. Role priming establishes the persona of a senior React and TypeScript expert, which guides the model to generate code reflecting best practices in design system architecture, including advanced TypeScript patterns and component design principles. This ensures the output is not just functional, but also idiomatic and maintainable.

Explicit constraints are crucial here. By clearly defining the as prop's behavior for button, a, and Link, and specifying the need for correct prop inference, the prompt forces the model to implement sophisticated TypeScript conditional types. This directly addresses the core challenge of type safety in polymorphic components, preventing common errors where incorrect props might be passed to the underlying element. Constraints on styling, testing, accessibility, and performance further ensure a comprehensive solution beyond just the core logic.

The request for structured output (file tree, separate type file, tests, accessibility, performance notes, example usage) compels the model to organize its response logically. This structure makes the generated solution immediately usable and understandable, mimicking how a human expert would deliver a complete component. It moves beyond a simple code snippet, providing a holistic view of the component's implementation, usage, and considerations, which is far more valuable than a single, undifferentiated block of code.

Model fit

Best AI models for this prompt

Claude

Claude's extensive context window handles complex TSX type definitions and component logic effectively. It excels at maintaining type inference across polymorphic components, making it suitable for this task. However, it may occasionally introduce minor type mismatches that require manual review. See the full Claude hub for deeper guidance.

ChatGPT

ChatGPT is proficient in generating clean React component code and accompanying unit tests. It manages TypeScript well, providing a solid foundation for the polymorphic button. For highly intricate generic type inference scenarios, some refinement might be necessary to achieve perfect type strictness. See the full ChatGPT hub for deeper guidance.

Gemini

Gemini rapidly produces functional code snippets and often includes helpful explanations. It can quickly scaffold the polymorphic component structure. Be aware that it may sometimes simplify complex type definitions, necessitating manual adjustments to ensure full adherence to strict type system requirements. See the full Gemini hub for deeper guidance.

When to use

  • When building a comprehensive design system requiring a single, stylistically consistent interactive primitive.
  • When consolidating multiple Button, Link, and Anchor components into one type-safe solution.
  • When strict TypeScript inference is crucial for developer experience and preventing prop-related bugs.
  • When needing to switch between button, a, or Link elements while maintaining visual consistency and shared logic.
  • When reducing boilerplate for interactive elements across a large React application.

When not to use

  • For very small projects with minimal interactive elements where a simple HTML button suffices.
  • When a component requires highly specialized, non-standard behavior that fundamentally differs from a button or link.
  • If your project does not use TypeScript, as the core benefit of type safety is lost.
  • When performance is absolutely critical for a single, high-frequency component, and the slight overhead of polymorphic types is unacceptable.

Get more from it

Pro tips

  • 1

    Always forward the `ref` to the underlying element, allowing consumers to directly interact with the DOM node or React component.

  • 2

    Carefully filter out internal props before spreading to the underlying element; this prevents invalid HTML attributes from being rendered.

  • 3

    Ensure comprehensive test coverage for each `as` prop variant, verifying correct prop inference and accessibility attributes.

  • 4

    When extending with custom components for the `as` prop, make sure their expected props are correctly merged into the component's types.

  • 5

    For complex styling, use a CSS-in-JS library that handles conditional styling based on the `as` prop and component variants effectively.

  • 6

    Prioritize semantic HTML for the default `button` and `a` elements to maintain inherent accessibility benefits.

  • 7

    Consider `React.memo` for the component if it frequently re-renders with the same props, optimizing performance in list contexts.

Don't ship this

Common mistakes

  • Forgetting to use `React.forwardRef` to pass the ref to the actual DOM element.

    Fix — Wrap the component definition with `React.forwardRef` and pass the `ref` parameter to the `as` component or element.

  • Not explicitly typing the `to` prop for the `Link` component when `as={Link}`.

    Fix — Ensure the conditional types correctly mark `to` as a required prop when `as` is the React Router `Link` component.

  • Inconsistent styling between the `button`, `a`, and `Link` variants.

    Fix — Apply a common base style to all variants and then layer specific styles based on `variant` and `size` props.

  • Spreading all props directly without filtering, leading to invalid HTML attributes.

    Fix — Destructure internal props like `variant` and `size` and only spread the remaining `rest` props to the rendered element.

  • Missing accessibility attributes for `<a>` or `Link` elements that visually act as buttons.

    Fix — Conditionally add `role='button'` and `tabIndex={0}` to `<a>` or `Link` elements when they function as interactive buttons.

  • Not handling default props gracefully for different `as` values, causing unexpected behavior.

    Fix — Implement conditional default props or ensure the base prop types are flexible enough to accommodate all `as` variants.

People also ask

Frequently asked questions

Q.Can I extend this polymorphic button to render other custom React components?

Yes, you can extend the PolymorphicComponentProps type to include additional React components. You'll need to define their specific prop interfaces and union them with the existing types to maintain type safety and inference for new as values.

Q.How does this component impact bundle size compared to separate `Button` and `Link` components?

The additional type complexity for polymorphism might slightly increase the TypeScript output, but this is usually negligible. The primary benefit is reduced overall code duplication and improved maintainability across your application, which often outweighs minor bundle size changes.

Q.Is this approach compatible with older versions of React Router?

The provided solution assumes React Router v6 due to its Link component's prop structure (specifically the to prop). If you are using an older version, you may need to adjust the Link component's prop types accordingly to match its specific API.

Q.How do I ensure consistent focus styles across all `as` variants?

Apply a consistent :focus-visible outline or styling to the root element of your Button component. This ensures that regardless of whether it renders as a button, a, or Link, the focus indicator is uniform and accessible.

Q.What if I need different event handlers or attributes for a specific `as` prop?

The type inference handles common attributes like onClick. For highly specific needs, you can augment the prop types to conditionally accept additional attributes or specialized event handlers based on the as prop value, ensuring strict typing.

Version 1.0Last reviewed July 20, 2026
Reviewed by PromptInFlow Editorial Team