Creating Variants from Base Components in React
Table of contents
Open Table of contents
Introduction
One of the best ways to build reusable and maintainable React components is by using base components with variants. This design pattern allows you to define a single, versatile base component that encapsulates core behavior and styles, and then extend it to create variant components with specialized behaviors and appearances. This approach ensures that your code is open for extension but closed for modification, adhering to best practices in component design.
In this article, we’ll walk through how to create and manage variants using base components in React.
The Concept of Base Components and Variants
A base component contains the shared structure, behavior, and styling logic that other components can inherit or extend. Variants are specialized versions of that base component, modified to suit different use cases, like different button types (e.g., primary, secondary, disabled).
By defining common behavior in the base component and extending it for specific use cases, you can achieve a more modular and reusable architecture.
Why Use Base Components with Variants?
Consistency: Base components ensure that your variants share a consistent structure and behavior. Maintainability: Changes to shared functionality are made in the base component, so you only need to update it once to propagate changes to all variants. Scalability: You can easily create new variants by extending the base component, without modifying its core behavior. Building a Base Component
Let’s start by creating a base component that handles core functionality, like rendering a button with common class names. This base component will accept additional props for flexibility and extension.
Example:
function ButtonBase({ className, as: Element = "button", ...restProps }) {
return (
<Element {...restProps} className={`common-button-classes ${className}`} />
);
}
In this example, the ButtonBase component renders a customizable button element. The className prop allows other components to add or override styles, and the as prop defines which HTML element will be used (e.g., a button, anchor, or div). The component is open for extension through the restProps, allowing any additional attributes (such as onClick) to be passed down.
Extending the Base Component to Create Variants
Once you’ve defined a base component, creating variants becomes simple. Each variant will import the base component, customize its appearance and behavior, and pass the necessary props.
Example:
function ButtonVariant({ variant = "primary", ...restProps }) {
const variantClassMap = {
primary: "btn-primary",
secondary: "btn-secondary",
danger: "btn-danger",
};
return <ButtonBase {...restProps} className={variantClassMap[variant]} />;
}
Here, ButtonVariant extends ButtonBase and adds variant-specific styling. The variantClassMap object maps each variant to its corresponding CSS class (e.g., primary, secondary, or danger). This allows you to render different types of buttons by simply changing the variant prop.
Usage Example:
function App() {
return (
<div>
<ButtonVariant
variant="primary"
onClick={() => alert("Primary clicked!")}
>
Primary Button
</ButtonVariant>
<ButtonVariant variant="secondary">Secondary Button</ButtonVariant>
<ButtonVariant variant="danger">Danger Button</ButtonVariant>
</div>
);
}
This approach allows you to create a consistent set of buttons that all share core behavior while looking and behaving differently based on the variant prop.
Advanced: Adding More Variants and Customization
As your design system grows, you might need to add more customization options, such as size, disabled state, or even different element types (e.g., anchor vs button). You can easily extend this base-variant structure to handle those cases.
Example with Additional Options:
function ButtonVariant({
variant = "primary",
size = "medium",
disabled = false,
...restProps
}) {
const variantClassMap = {
primary: "btn-primary",
secondary: "btn-secondary",
danger: "btn-danger",
};
const sizeClassMap = {
small: "btn-small",
medium: "btn-medium",
large: "btn-large",
};
return (
<ButtonBase
{...restProps}
className={`${variantClassMap[variant]} ${sizeClassMap[size]} ${disabled ? "btn-disabled" : ""}`}
disabled={disabled}
/>
);
}
With this, you can now control button size, variant, and disabled state directly through props.
Usage Example:
function App() {
return (
<div>
<ButtonVariant variant="primary" size="large">
Large Primary Button
</ButtonVariant>
<ButtonVariant variant="secondary" size="small" disabled>
Disabled Small Button
</ButtonVariant>
</div>
);
}
Conclusion
Using base components with variants in React offers a powerful pattern for building reusable, scalable UI components. By encapsulating core behavior in the base component and allowing extensions through variants, you maintain consistency across your app while enabling flexibility and customization.
This approach aligns with React’s “composition over inheritance” principle and helps you build a design system that is both maintainable and easy to extend.