Typing Components That Wrap Native HTML Elements
Learn to type components that extend native HTML elements — buttons, inputs, divs, and beyond — using React's per-element attribute interfaces, so wrapper components stay fully compatible with the native element they're built on.
A huge share of real-world components are thin wrappers around a native HTML element: a styled Button around <button>, a validated TextField around <input>, a Card around <div>. Each of these should ideally accept every prop the underlying native element accepts — onClick, disabled, aria-label, data-testid, and dozens more — without the component author manually re-declaring each one. React's DOM typings solve this with a family of per-element attribute interfaces, and knowing how to extend, narrow, and merge them correctly is a core React + TypeScript skill.
The per-element attribute interfaces
React's type definitions include one interface for each category of HTML element's valid attributes: React.ButtonHTMLAttributes<HTMLButtonElement> for <button>, React.InputHTMLAttributes<HTMLInputElement> for <input>, React.AnchorHTMLAttributes<HTMLAnchorElement> for <a>, and a broad React.HTMLAttributes<HTMLDivElement> covering the general attributes shared by most elements — class, id, style, children, event handlers, and standard ARIA attributes — used for elements like <div>, <span>, and <section> that don't have many attributes beyond the universal set.
Each interface is generic over the specific DOM element type it targets, which matters most for event handlers: ButtonHTMLAttributes<HTMLButtonElement>'s onClick is typed as MouseEventHandler<HTMLButtonElement>, so inside the handler, event.currentTarget is correctly typed as HTMLButtonElement rather than a generic Element. Using the wrong element type parameter — say, HTMLAttributes<HTMLButtonElement> instead of ButtonHTMLAttributes<HTMLButtonElement> — still compiles but silently loses button-specific attributes like type="submit" and disabled.
These interfaces already extend DOMAttributes<T>, which supplies all the common event handler props (onClick, onChange, onFocus, and so on) and AriaAttributes, which supplies every standard aria-* prop — so you get accessibility-attribute type checking for free just by extending the right interface, with no extra work.
Extending native props for your own components
The standard pattern is to intersect the relevant native attributes interface with your own custom props: type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { variant: 'primary' | 'secondary' }. This gives the resulting type both the full native surface and your addition, and if a name collides — say you also want a prop called variant that isn't a real HTML attribute — TypeScript resolves the intersection cleanly since variant doesn't exist on ButtonHTMLAttributes in the first place.
When a custom prop needs the same name as a native one but a different, incompatible type, plain intersection breaks down — intersecting two conflicting types for the same key collapses to never, making the prop impossible to satisfy. The fix is Omit: strip the native prop out first, then add your own version back in. For example, a Rating component built on <input> where you want value to be typed as a specific number range rather than the native string | number | readonly string[] value: type RatingProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> & { value: 1 | 2 | 3 | 4 | 5 }.
This Omit-then-add pattern is one of the most common moves in real component libraries, because it lets you keep 95% of a native element's behavior while deliberately tightening or replacing the one or two props your component wants to control more strictly than the DOM does.
Ref compatibility with native elements
A component that wraps a native element and wants to expose that element via ref — so a consumer can call .focus() on a wrapped input, for instance — needs forwardRef typed with the DOM element as its first type argument: forwardRef<HTMLInputElement, TextFieldProps>((props, ref) => <input ref={ref} {...props} />). This makes the ref a RefObject<HTMLInputElement> (or the more general ForwardedRef<HTMLInputElement> inside the callback) at every call site, giving consumers the same DOM API they'd get from a plain <input>.
It's important that the ref's type argument matches the actual DOM node being rendered — forwarding a ref typed as HTMLInputElement but attaching it to a <div> inside the component is a type-checking error and, more importantly, a real bug, since .value and .focus() wouldn't behave as expected on a div.
When a wrapper component doesn't render a single native element directly (for example, it renders a <label> wrapping an <input>), you choose which underlying node the ref should expose — usually the interactive element itself, HTMLInputElement, rather than the outer wrapping element, since that's what most consumers actually want to call .focus() or read .value from.
Components that fully behave like native elements
A well-typed wrapper component should be a drop-in replacement for the native element it wraps wherever reasonable — accepting the same events, the same accessibility attributes, the same styling props — so that consumers don't need to remember 'this custom Button is missing onMouseEnter' or similar gaps. Extending the correct *HTMLAttributes<T> interface and spreading ...rest onto the underlying element is what achieves this in one motion, rather than manually listing out the handful of props you happen to remember.
It's common, though, to deliberately narrow the native surface for safety or design-system consistency — for instance, a design-system Button might intentionally omit the native style prop (to stop consumers from bypassing the design system's spacing and color tokens) using Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style'>. This is a conscious trade-off: full native compatibility versus enforcing your own component's constraints, and the choice should be explicit rather than accidental.
Extending, narrowing, and ref-forwarding native attributes
React.HTMLAttributes<HTMLDivElement> is the right choice for generic containers (div, span, section) that don't have element-specific attributes. Always match the DOM element type in forwardRef's first type argument to the element the ref is actually attached to.
// 1. Extend: add your own prop alongside every native one
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant: 'primary' | 'secondary';
};
function Button({ variant, className, ...rest }: ButtonProps) {
return <button className={`btn btn-${variant} ${className ?? ''}`} {...rest} />;
}
// 2. Omit + replace: override a native prop's type
type RatingProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> & {
value: 1 | 2 | 3 | 4 | 5;
onChange: (value: 1 | 2 | 3 | 4 | 5) => void;
};
// 3. Ref-forwarding wrapper: exposes the real HTMLInputElement
const TextField = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
(props, ref) => <input ref={ref} {...props} />
);Code Examples
HTMLAttributes<HTMLDivElement> supplies every general-purpose attribute a div can take, so Card accepts click handlers, ARIA attributes, and standard HTML props automatically, with only elevated needing to be declared by hand.
Omit<..., 'type'> removes the native type attribute's broader string type so it can be re-declared as the narrower 'button' | 'submit' union, and re-declaring 'aria-label' as required (rather than the native optional version) enforces an accessibility rule at compile time.
type IconButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> & { ... };
Common mistake: Forgetting to Omit 'type' before redeclaring it would produce a type conflict, since the native type attribute's type and the narrower union aren't assignable to each other within a plain intersection.
type CardProps = React.HTMLAttributes<HTMLDivElement> & {
elevated?: boolean;
};
function Card({ elevated, className, ...rest }: CardProps) {
return (
<div
className={`card ${elevated ? 'card-elevated' : ''} ${className ?? ''}`}
{...rest}
/>
);
}
// Usage — onClick, onMouseEnter, id, aria-label all work because
// they come from HTMLAttributes<HTMLDivElement>
<Card elevated onClick={() => console.log('clicked')} aria-label="Profile summary" />;// Narrow the native `type` attribute to a safer union,
// and require an aria-label since icon-only buttons need one.
type IconButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> & {
type?: 'button' | 'submit';
icon: React.ComponentType<{ size?: number }>;
'aria-label': string;
};
function IconButton({ icon: Icon, type = 'button', ...rest }: IconButtonProps) {
return (
<button type={type} {...rest}>
<Icon size={18} />
</button>
);
}
// Compile error: missing required aria-label
// <IconButton icon={TrashIcon} onClick={handleDelete} />;
// OK
<IconButton icon={TrashIcon} onClick={handleDelete} aria-label="Delete item" />;Extend vs. Omit-and-replace
Use plain intersection for new props; Omit-then-add when your prop reuses a native name.
Adding a NEW prop (no name clash)
ButtonHTMLAttributes<HTMLButtonElement> & { variant: ... }
└─ plain intersection is enough
Overriding an EXISTING prop's type (name clash)
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> & { onChange: (v: number) => void }
└─ strip the native prop first, then add your stricter versionImportant Rules
Extend the interface that matches the actual native element you're rendering (ButtonHTMLAttributes, InputHTMLAttributes, etc.) — not a mismatched or overly generic one — so event and attribute types stay accurate.
Use Omit<NativeAttrs, 'propName'> before re-declaring a prop that shares a name with a native attribute but needs a different type.
When forwarding a ref, match forwardRef's element type argument to the actual DOM node the ref is attached to inside the component.
Spread ...rest onto the underlying native element so consumers get full native behavior (event handlers, ARIA attributes, data-* attributes) without you re-declaring each one.
Where this matters
Virtually every component library — internal design systems and public ones alike — is built on this exact pattern: a Button, Input, Textarea, or Link component that extends the matching native attributes interface, narrows one or two props for design-system rules, and forwards a ref to the underlying DOM node. Getting this right is what lets a <Button> be used everywhere a <button> could be used — inside forms, with onClick, wrapped in a tooltip that needs a ref — without consumers hitting missing-prop errors.
type ButtonProps = React.HTMLAttributes<HTMLButtonElement> & { variant: string };
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { variant: string };
HTMLAttributes only covers general attributes shared across elements; it's missing button-specific ones like type and disabled. ButtonHTMLAttributes includes those in addition to everything HTMLAttributes provides.
type RatingProps = React.InputHTMLAttributes<HTMLInputElement> & { value: 1 | 2 | 3 | 4 | 5 };
type RatingProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> & { value: 1 | 2 | 3 | 4 | 5 };
Intersecting without Omit creates a conflicting value type (native string|number|readonly string[] combined with the numeric literal union), which TypeScript collapses to never — making the prop impossible to satisfy with any real value.
[multiple-choice] Why would you choose React.HTMLAttributes<HTMLDivElement> over React.ButtonHTMLAttributes<HTMLButtonElement> for a Card component wrapping a <div>? A. HTMLAttributes is deprecated so it should be avoided B. HTMLAttributes covers the general attributes shared by most elements, which matches what a plain div actually supports C. They are exactly identical and it doesn't matter D. ButtonHTMLAttributes works for any HTML element
B. HTMLAttributes covers the general attributes shared by most elements, which matches what a plain div actually supports — HTMLAttributes<T> supplies the general-purpose props (class, id, style, event handlers, ARIA) that most elements share, which is the right match for a div that has no element-specific attributes beyond that set.
[multiple-choice] Why is Omit needed before redeclaring a prop like value with a narrower type than the native attribute has? A. Omit makes the code run faster B. Without it, intersecting two incompatible types for the same key collapses to never, making the prop unsatisfiable C. TypeScript requires Omit for all custom props D. Omit is only needed for optional props
B. Without it, intersecting two incompatible types for the same key collapses to never, making the prop unsatisfiable — A plain intersection of two conflicting types for the same property key resolves to never; Omit removes the native version first so the new, narrower type can take its place cleanly.
[true-false] True or false: forwardRef's first type argument should always be HTMLElement regardless of which specific element the ref is attached to. A. True B. False
B. False — It should match the actual DOM node type the ref attaches to (e.g. HTMLInputElement for an input), so consumers get the correct, specific DOM API like .focus() or .value with proper typing.
Type a Link component that wraps <a>, accepts all native anchor attributes, forwards a ref to the underlying HTMLAnchorElement, and additionally requires href (native anchor attributes make href optional).
type LinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { href: string; }; const Link = React.forwardRef<HTMLAnchorElement, LinkProps>( (props, ref) => <a ref={ref} {...props} /> );
Start from React.AnchorHTMLAttributes<HTMLAnchorElement>, Omit 'href', then add it back as a required string. Use forwardRef<HTMLAnchorElement, LinkProps>.