{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreih5w4uxwukjmhaqzfig4ttfgkzwjjfuy7ouo5ce4zz3a3avycqe6q",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3movgqcuqaet2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreihd2tggji6trcuohrzybyehpq2pryavmy7hu3f3bwyqjpsj6wo7pe"
    },
    "mimeType": "image/webp",
    "size": 122392
  },
  "path": "/sandhyaallgandhuala/how-to-build-a-reusable-button-component-in-react-with-typescript-4h95",
  "publishedAt": "2026-06-22T17:13:28.000Z",
  "site": "https://dev.to",
  "tags": [
    "react",
    "typescript",
    "webdev",
    "beginners"
  ],
  "textContent": "##  Introduction\n\nWhen I started building my Smart Budget Tracker app, I noticed I was copy-pasting button code everywhere - submit buttons, link buttons, loading buttons. Each one looked slightly different. That's when I decided to build one reusable button component to rule them all.\n\nIn this post I'll walk you through how I built it using React and TypeScript.\n\n##  What We are Building\n\nA single component that handles:\n\n  1. Regular click buttons\n  2. From submit buttons\n  3. Navigation link buttons\n  4. Loading state with a spinner\n  5. Disabled state\n  6. Multiple sizes and variants\n\n\n\n##  Step 1 - Defines the Props Interface\n\nThe first thing I do in Typescript is define exactly what the component accepts. This gives you autocomplete and catches mistakes at compile time.\n\n\n\n    interface Props {\n      label: string;\n      onClick?: () => void;\n      disabled?: boolean;\n      loading?: boolean;\n      href?: string;\n      variant?: 'default' | 'primary';\n      size?: 'sm' | 'md' | 'lg';\n      fullWidth?: boolean;\n      type?: 'submit' | 'button';\n      className?: string;\n    }\n\n\n\nThe ? means the prop is optional, only label is required everything else has a default.\n\n##  Step 2 - Set Default Values\n\n\n    function Button({\n      label,\n      onClick,\n      disabled = false,\n      loading = false,\n      href,\n      variant = 'default',\n      size = 'md',\n      fullWidth = false,\n      type = 'button',\n      className = '',\n    }: Props) {\n\n\n\nDefault values mean callers don't need to pass every prop, `<Button label = \"Save\"/>` just works.\n\n##  Step 3 - Build the CSS class dynamically\n\nInstead of writing if/else for every style combination, I build the class string from an array\n\n\n\n    const isDisabled = disabled || loading;\n\n    const wrapperCSSClass = [\n      'btn-base',\n      `btn-${variant}`,\n      `btn-${size}`,\n      fullWidth ? 'btn-full': '',\n      isDisabled ? 'btn-disabled': '',\n      className,\n    ].filter(Boolean).join(' ');\n\n\n\nfilter(Boolean) removes any empty string so you don't get extra spaces in the class name. Adding a new variant is just one line.\n\n##  Step 4 - Handle the Loading Spinner\n\nWhen Loading is true, I show a spinner SVG icon and change the label text\n\n\n\n    const labelContent = (\n      <>\n        {loading && (\n          <svg className=\"animate-spin h-4 w-4\" ...>\n            ...\n          </svg>\n        )}\n        <span>{loading ? 'Loading...': label}</span>\n      </>\n    );\n\n\n\nI also added `aria- busy = {loading}` on the button element. This tells screen readers that the button is busy - a small but important accessibility detail.\n\n##  Step 5 - Handle Link Vs Button\n\nThis was the interesting part, sometimes a button navigates to another pages like a \"Registration\" link that looks like a button. I handle both cases:\n\n\n\n    if (href) {\n      return isDisabled\n        ? <span className={wrapperCSSClass}>{labelContent}</span>\n        : <Link to={href} className={wrapperCSSClass}>{labelContent}</Link>;\n    }\n\n    return (\n      <button\n        type={type}\n        onClick={onClick}\n        disabled={isDisabled}\n        className={wrapperCSSClass}\n        aria-busy={loading}\n      >\n        {labelContent}\n      </button>\n    );\n\n\n\nWhen href is passed, it renders a React Router `<Link>`. When disabled, it renders a `<span>` because a disabled link is semantically incorrect in HTML.\n\n##  How to Use it\n\n\n    // Primary submit button\n    <Button label=\"Log in\" type=\"submit\" variant=\"primary\" fullWidth loading={loading} />\n\n    // Navigation link styled as button\n    <Button label=\"Register Free\" href=\"/Register\" />\n\n    // Disabled button\n    <Button label=\"Save\" disabled />\n\n\n\n##  What I Learned\n\n  1. TypeScript interfaces makes component self-documenting - you always know what props are available\n  2. filter(Boolean) is a clean trick for building dynamic class strings\n  3. One component can handle both `<button>` and `<Link>`with a simple conditional render\n  4. Accessibility (aria-busy) is easy to add and makes a real difference\n\n\n\n##  What's Next\n\nIn my next post l'll cover how I built a reusable TextInput component with error state, icons, and password toggle - also from my Smart Budget Tracker project.\n\nIf this helped you, drop a like or comment. I'm just getting started with blogging and any feedback is welcome!\n\n\n",
  "title": "How to Build a Reusable Button Component in React with TypeScript"
}