Skip to content

Improve ref type safety for polymorphic components when using semantic tags with as #3828

@hansolc

Description

@hansolc

What package within Headless UI are you using?
@headlessui/react

What version of that package are you using?
v2.2.9

What browser are you using?
Chrome

Describe your issue
Headless UI components currently accept refs using a broad type signature like:

ref?: Ref<HTMLElement>

This works, but it does not provide strict ref type validation when the as prop is set to a specific HTML tag.

<Button as="button" ref={useRef<HTMLAnchorElement>(null)} />

This should ideally produce a type error, because:

  • as="button" → the ref should be HTMLButtonElement
  • but the user passed HTMLAnchorElement

However, since Headless UI uses Ref<HTMLElement>, the incorrect ref type is allowed.

Because Headless UI encourages polymorphism (as="button", as="a", custom components, and Fragment), I would like to propose an approach that enables strict ref inference only when as is a semantic HTML tag, without breaking the flexibility Headless UI currently provides.

Proposed solution
My suggestion is to enhance this by adding TTag and conditionally enforcing strict ref types only when the tag is a semantic HTML element:

export type RefProp<
  T extends Function,
  TTag extends ElementType = any,
> =
  // If TTag is an intrinsic HTML element, infer the correct ref type
  TTag extends keyof JSX.IntrinsicElements
    ? {
        ref?: Ref<
          JSX.IntrinsicElements[TTag] extends React.DetailedHTMLProps<
            React.HTMLAttributes<infer TElement>, any
          >
            ? TElement
            : never
        >
      }
    : // Otherwise, fall back to Headless UI's existing behavior
    T extends (props: any, ref: Ref<infer RefType>) => any
      ? { ref?: Ref<RefType> }
      : never;

Then apply the updated RefProp in each component’s internal typing—for example, for Button:

export interface _internal_ComponentButton {
  <TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
    props: ButtonProps<TTag> & RefProp<typeof ButtonFn, TTag>
  ): JSX.Element
}

Example outcome

const TestButton = () => {
  const ref = useRef<HTMLButtonElement>(null)

  // ❌ Type error:
  // as="a" → expected HTMLAnchorElement
  return <Button as="a" ref={ref}>test</Button>
}

If you'd like, I can help you write a clean PR description too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions