Learn

/

Extending HTML

Extending HTML

5 patterns

Forwarding native HTML attributes with ComponentProps, Omit, and polymorphic as props.

Custom button component props
medium
Avoid
interface ButtonProps
  extends React.ComponentProps<'button'> {
  /** The button text. */
  label: string;
  /** Click handler. */
  onClick?: () => void;
  /** Whether the button is disabled. */
  disabled?: boolean;
  /** Button type. @default 'button' */
  type?: 'button' | 'submit' | 'reset';
}

Prefer
interface ButtonProps
  extends React.ComponentProps<'button'> {
  /** The button's visible text content. */
  label: string;
  /** Visual style variant. @default 'primary' */
  variant?: 'primary' | 'secondary' | 'danger';
  /** Button size. @default 'md' */
  size?: 'sm' | 'md' | 'lg';
}

When you extend ComponentProps<'button'>, onClick, disabled, and type are already included with correct types. Only add props that the native element doesn't have — label, variant, and size are genuine additions. Re-declaring inherited props clutters the API.

React Docs: Common Components
Input wrapper with custom onChange
medium
Avoid
interface TextFieldProps
  extends Omit<
    React.ComponentProps<'input'>,
    'onChange' | 'value'
  > {
  /** Controlled string value. */
  value: string;
  /** Called on input change with the full event. */
  onChange: (
    event: React.ChangeEvent<HTMLInputElement>
  ) => void;
}

Prefer
interface TextFieldProps
  extends Omit<
    React.ComponentProps<'input'>,
    'onChange'
  > {
  /**
   * Called with the new string value on change.
   * Simplified from the native event-based onChange.
   */
  onChange: (value: string) => void;
}

Omit should only remove props you actually need to redefine. The native value on <input> already works fine — omitting and re-declaring it as string is unnecessary.

The real win is simplifying onChange to pass just the string value, so consumers write onChange={setValue} instead of onChange={(e) => setValue(e.target.value)}.

TypeScript: Omit Utility Type
Polymorphic component typing
hard
Avoid
type CardProps<
  E extends string = 'div'
> = {
  /** The HTML element to render as. */
  as?: E;
  children: React.ReactNode;
} & Omit<
  React.HTMLAttributes<HTMLElement>,
  'as' | 'children'
>;

Prefer
type CardProps<
  E extends React.ElementType = 'div'
> = {
  /** The HTML element or component to render. */
  as?: E;
  children: React.ReactNode;
} & Omit<
  React.ComponentPropsWithoutRef<E>,
  'as' | 'children'
>;

React.ElementType constrains as to valid elements/components, and ComponentPropsWithoutRef<E> adapts the available props based on the element. as="a" enables href; as="button" enables type.

Compare string + HTMLAttributes<HTMLElement> which accepts any string and always gives generic div-like props regardless of the element.

React Docs: createElement
Wrapper div component props
easy
Avoid
interface CardProps {
  children: React.ReactNode;
  className?: string;
  id?: string;
  style?: React.CSSProperties;
  role?: string;
  'aria-label'?: string;
}

Prefer
interface CardProps
  extends React.ComponentProps<'div'> {
  children: React.ReactNode;
}

Extending React.ComponentProps<'div'> inherits every valid div attribute — className, style, role, all ARIA props, event handlers, and more. Only declare custom props that don't exist on the native element.

React Docs: Common Components
Link component extending anchor
easy
Avoid
interface LinkProps {
  href: string;
  children: React.ReactNode;
  target?: string;
  rel?: string;
  className?: string;
  onClick?: () => void;
}

Prefer
interface LinkProps
  extends React.ComponentProps<'a'> {
  /**
   * Opens in a new tab with noopener noreferrer.
   * @default false
   */
  isExternal?: boolean;
}

Extending <a> gives you href, target, rel, download, ARIA props, and all event handlers for free. The custom isExternal prop adds real value — it encapsulates the target="_blank" rel="noopener noreferrer" pattern into a single boolean.

React Docs: Common Components