Tag

Renders a polymorphic element chosen at runtime, with that element's own props fully typed.

API

Props

proptypedefault
as?ElementType"div"the element or component to render
…restprops of astyped against the chosen element

Examples

Usage

Everything that is not as is forwarded to the rendered element untouched, and typed against it: with as="a" the compiler knows about href, with as="section" it rejects it.

tsx
<Tag as="section" className="prose">
  {children}
</Tag>

<Tag as="a" href="/about" rel="noreferrer">
  About
</Tag>

Details

TagProps

The exported TagProps<T> type builds prop types for your own polymorphic components.

tsx
type ButtonProps<T extends ElementType = "button"> = TagProps<T> & { active?: boolean };

It takes a second parameter for the props your component adds on top of the element. Those own props win over native ones of the same name: they replace them rather than merging with them, so a component that gives onClick its own meaning does not end up with two conflicting signatures for it.

tsx
type ButtonOwnProps = {
  active?: boolean;
  onClickBlur?: MouseEventHandler<HTMLElement>;
};

type ButtonProps<T extends ElementType = "button"> = TagProps<T, ButtonOwnProps>;

Tag.forward

A component built on Tag destructures its own props and has to hand the rest over. Spreading that rest directly does not type-check: TypeScript cannot prove that the leftovers of a generic component match ComponentProps<T>, because T stays unresolved until someone instantiates the component. Tag.forward is where that assertion lives, once, instead of in every component built on Tag.

tsx
Tag.forward<T extends ElementType>(props: object, fallback?: ElementType): TagProps<T>;
tsx
export const Card = <T extends ElementType = "div">({ className, children, ...rest }: CardProps<T>) => (
  <Tag {...Tag.forward<T>(rest)} className={cn(cardBaseClassName, className)}>
    {children}
  </Tag>
);

It leaves as alone: whatever the consumer chose passes through untouched. The optional second argument is the element to fall back to when the consumer chose nothing — pass it when your component renders something other than a div by default.

tsx
<Tag {...Tag.forward<T>(rest, "button")} onClick={handleClick} />

Because the fallback only applies in the absence of as, a Button stays a button by default and still becomes a router Link the moment someone asks for one, without the component knowing anything about routing.

The default element

Without as the element falls back to a div, so a component built on Tag is never in an invalid state while the consumer has not chosen yet.