{
"$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"
}