Reusable Button component. Build Once, Use Everywhere.

Reusable Button component. Build Once, Use Everywhere.

Building a Reusable Button Component in React

Creating a reusable button component in React is a fundamental task for any developer aiming to streamline UI development. A well-designed button component can be used across multiple projects, saving time and ensuring consistency.

In this blog, we'll build a flexible React button component that supports different variants, sizes, and element types (button or anchor). I'll explain the code, highlight its reusability, and provide ready to use code snippet.

Why is it important?

Buttons are a core part of any user interface, appearing in forms, navigation bars, links, and more. A reusable button component allows developers to:

  • Reduce repetitive code by centralizing button logic.

  • Maintain consistent styling across the application.

  • Easily customize buttons with props for variants, sizes, and other functionality.

  • Support both <button> and <a> elements for flexibility in navigation or actions.

NOTE

We use Tailwind version 4

Key Features + Code Snippet

  • Variants: Easily switch between primary, secondary, outline, and link stylings.

  • Sizes: Choose between large (lg) and medium (md) out of the box. Expand easily to support more.

  • Polymorphic: Render as either a standard <button> for actions or an <a> for links, with proper external link handling.

  • TypeScript Safety: All props are type-checked, and required fields (e.g., href for <a>) are enforced.

  • Customizable: Add more variants, sizes, or class overrides as your design system evolves.

Code Snippet

import type { ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode, FC } from "react"

import { cn } from "@/shared/utils"

const buttonVariants = {
  primary: "text-gray-50 bg-cyan-700 hover:bg-cyan-800",
  secondary: "text-gray-50 bg-blue-700 hover:bg-blue-800",
  outline: "text-gray-50 border border-current hover:bg-gray-100 hover:text-gray-900",
  // learn more about tailwind groups: https://tailwindcss.com/docs/hover-focus-and-other-states#differentiating-nested-groups
  link: "group/link text-gray-100 ",
}

const buttonSizes = {
  lg: "px-4 py-3 rounded-none text-base ",
  md: "px-3 py-2 rounded-none text-sm ",
}

type CommonProps = {
  variant?: keyof typeof buttonVariants;
  size?: keyof typeof buttonSizes;
  classes?: string;
  children: ReactNode;
};

type ButtonProps = {
  element?: "button";
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> & CommonProps;

type AnchorProps = {
  element: "a";
  href: string;                // <-- required when using 'a'
  isExternal?: boolean;
} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "className"> & CommonProps;

type Props = ButtonProps | AnchorProps;

const ContentWrapper = ({ children }: { children: ReactNode }) => (
  <span className="relative inline-block group-hover/link:underline group-hover/link:underline-offset-4">
    {children}
  </span>
);

const Button: FC<Props> = ({ variant = "primary", size = "lg", classes, element, children, ...rest }) => {
  const className = cn(
    "relative max-h-max leading-[0] inline-flex items-center font-semibold text-nowrap transition",
    buttonSizes[size],
    buttonVariants[variant],
    classes,
  )

  if (element === "a") {
    const { isExternal, ...anchorProps } = rest as AnchorProps
    return (
      <a
        {...anchorProps}
        className={className}
        target={isExternal ? "_blank" : undefined}
        rel={isExternal ? "noopener noreferrer" : undefined}
      >
        <ContentWrapper>
          {children}
        </ContentWrapper>
      </a>
    )
  }

  // Default: button
  const buttonProps = rest as ButtonProps
  return (
    <button {...buttonProps} type="button" className={className}>
      <ContentWrapper>
        {children}
      </ContentWrapper>
    </button>
  )
}

export default Button

Check out my YouTube video where I explain its functionality

Code Explanation

Let's break down the key parts of the code to understand how it achieves reusability and flexibility.

  • The buttonVariants object defines three variants (you can add more): primary, secondary, and tertiary, each with specific classes. This object contains styles related to colors, shadow, hover/focus effects.

  • The buttonSizes object defines two sizes (you can add more): lg, md. It contains styles related to dimensions and spacing font-size, height, width, padding, border-radius etc.

  • ContentWrapper is a helper component designed to wrap the visible content (children) inside reusable Button, providing consistent styling for hover underline effects—especially important for the link variant.

Summary

Here we build a truly reusable Button component using Tailwind. You only need to style once, and then reuse everywhere in your app. The code is type-safe, simple to drop in, and easy to extend when design needs change.