How to Extend HTML Elements in React (Typescript)
Jasser Mark Arioste
In one of my projects I often ended up extending html elements when using a css library like daisyUI. DaisyUI is great because it’s so flexible and it uses html elements only. It’s not like MaterialUI where a <button> has it’s own react component and has a lot of built-in functionality.
In other words, sometimes we want to create a <Button> component that gets all the properties of <button> and has extra functionalities or props.
Prerequisites:
- Basic knowledge of generics in typescript
- Basic knowledge of react components
Wrapping an HTML Element into a component
If we need to wrap an html element we can simply extend the generic interface React.ComponentPropsWithoutRef<T> for our props and pass it onto our component. For example if we want to create a button with a start icon in daisyUI. This is a overly simplified example:
//Button.tsx
import React from "react";
//create the button Props
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
startIcon?: React.ReactNode;
//write other extra props here...
}
const Button = ({ startIcon, children, ...props }: ButtonProps) => {
//render the button
return (
<button {...props} className="btn gap-2">
{startIcon}
{children}
</button>
);
};
export default Button;
And how to use it:
import Button from "../components/Button";
import { FiDownload } from "react-icons/fi";
const Component = () => {
return (
<div className="container mx-auto my-4">
<Button>
<FiDownload size={20} />
Download
</Button>
</div>
);
};
export default Component;
Result:
What are the cons of wrapping an html element?
Everything above looks good however we should always weigh the pros and cons. If we merely want to add an icon to a button, we can simply do without a extra layer of component.
import { FiDownload } from "react-icons/fi";
...
<button className="btn gap2">
<FiDownload size={20} />
Download
</button>
...
Below are the cons of wrapping html elements:
- Every wrapped html element is much slower to render compared to regular html + classes. It might be negligible but this is just a thing to keep in mind.
- Every layer of abstraction creates a constraint that will be harder to maintain in the future. It creates restrictions and less flexibility.
When should you extend html elements or components?
You should extend html elements when the component is widely used in the application and has a very different behavior that is hard than just regular html + classes. One example is if we want to add special effects like “ripple effect” to a button.
//ButtonWithRipple.tsx
import React, { CSSProperties, useEffect, useState } from "react";
import { useDebounce } from "usehooks-ts";
//create the button Props
export interface ButtonWithRipple
extends React.ComponentPropsWithoutRef<"button"> {}
const ButtonWithRipple = ({
children,
onClick,
className,
...props
}: ButtonWithRipple) => {
const [ripples, setRipples] = useState<CSSProperties[]>([]);
const _debounced = useDebounce(ripples, 2000);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
var rect = e.currentTarget.getBoundingClientRect();
var left = e.clientX - rect.left;
var top = e.clientY - rect.top;
const height = e.currentTarget.clientHeight;
const width = e.currentTarget.clientWidth;
const diameter = Math.max(width, height);
setRipples([
...ripples,
{
top: top - diameter / 2,
left: left - diameter / 2,
height: Math.max(width, height),
width: Math.max(width, height),
},
]);
if (onClick) {
onClick(e);
}
};
useEffect(() => {
if (_debounced.length) {
setRipples([]);
}
}, [_debounced.length]);
//render the button
return (
<button
{...props}
onClick={handleClick}
className={className + " relative no-animation overflow-hidden"}
>
{ripples.map((ripple, index) => {
return (
<div
key={index}
className="absolute bg-gray-50 rounded-full opacity-25 ripple"
style={ripple}
></div>
);
})}
{children}
</button>
);
};
export default ButtonWithRipple;
Add ripple animation:
.ripple {
transform: scale(0);
animation: ripple 600ms linear;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
Usage:
<ButtonWithRipple className="btn gap-2">
<FiDownload size={20} />
Download
</ButtonWithRipple>
Conclusion
We successfully created a component that extends the default html button. We also learned on what are the cons of extending html elements and when to extend an html element to a react component that we can use anywhere in our application.


