A React button component is a small function that renders a <button> and receives its label as children and its behavior as an onClick prop. Once it also accepts a variant, a size and a loading state, and forwards every other prop to the real button, you have one reusable button component for the whole app. This guide builds it step by step, explains type="button" vs submit inside forms, passing arguments to click handlers, and a typed version in TypeScript.
Every example was built with React 19.3.0, Vite 8.3.0 and TypeScript 7.0.2 and opened in Microsoft Edge; buttons were clicked with real mouse events and the outputs are the browser console messages or tsc output. Reference: Responding to Events on react.dev and <button> on MDN.
A simple React button component
import { useState } from "react";
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<Button onClick={() => setCount(count + 1)}>Clicked {count} times</Button>
</div>
);
}
A reusable Button with variants, sizes, icons and loading
The component below keeps the look in one place. Destructure the props it handles itself and pass the rest (type, onClick, id, aria-*) straight to the <button> with {...rest}, so it works like a normal button:
// Button.jsx
const styles = {
base: { border: "none", borderRadius: 6, cursor: "pointer", fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 6, margin: 4 },
variant: {
primary: { background: "#2563eb", color: "white" },
secondary: { background: "#e5e7eb", color: "#111827" },
danger: { background: "#dc2626", color: "white" },
},
size: { small: { padding: "4px 10px", fontSize: 13 }, medium: { padding: "8px 16px", fontSize: 15 }, large: { padding: "12px 22px", fontSize: 17 } },
};
export default function Button({ variant = "primary", size = "medium", loading = false, icon, disabled, children, ...rest }) {
return (
<button
{...rest} // type, onClick, aria-label, ...
disabled={disabled || loading}
style={{ ...styles.base, ...styles.variant[variant], ...styles.size[size], opacity: disabled || loading ? 0.6 : 1 }}
>
{icon}
{loading ? "Saving..." : children}
</button>
);
}
// App.jsx
import { useState } from "react";
import Button from "./Button";
export default function App() {
const [saving, setSaving] = useState(false);
return (
<div>
<p>
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger" onClick={() => console.log("delete clicked")} id="delete">Delete</Button>
</p>
<p>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</p>
<p>
<Button icon={<span>⬇</span>} variant="secondary">Download</Button>
<Button disabled>Disabled</Button>
<Button loading={saving} onClick={() => setSaving(true)} id="save">Save</Button>
</p>
</div>
);
}
Console output after clicking Delete and Save:
delete clicked
- Disable while loading so a double click cannot submit twice.
- Keep a real
<button>: it is focusable and works with the keyboard; a clickable<div>is not. - Icon-only buttons need an
aria-label, which...restpasses through.
Buttons inside forms: type=”button” vs submit
A <button> without a type inside a form is a submit button. Give buttons that should not submit type="button":
import { useState } from "react";
export default function App() {
const [log, setLog] = useState([]);
const add = (msg) => { console.log(msg); setLog((l) => [...l, msg]); };
return (
<form onSubmit={(e) => { e.preventDefault(); add("form submitted"); }}>
<input defaultValue="ana@example.com" />
<button id="default">Default button</button> {/* type="submit" inside a form */}
<button type="button" id="plain" onClick={() => add("type=button: no submit")}>Preview</button>
<button type="reset">Reset</button>
<ul>{log.map((m, i) => <li key={i}>{m}</li>)}</ul>
</form>
);
}
Console output after clicking “Default button” and “Preview”:
form submitted
type=button: no submit
type="button" did not.Pass arguments to an onClick handler
Wrap the call in an arrow function. Writing onClick={choose(plan)} would call the function during render instead of on click:
const plans = ["Basic", "Pro", "Team"];
export default function App() {
function choose(plan, event) {
console.log(`chose ${plan} (clicked <${event.currentTarget.tagName.toLowerCase()}>)`);
}
return (
<div>
{plans.map((plan) => (
<button key={plan} id={plan} onClick={(e) => choose(plan, e)}>{plan}</button> // arrow function passes the argument
))}
</div>
);
}
Console output after clicking Pro and Team:
chose Pro (clicked <button>)
chose Team (clicked <button>)
A typed button component in TypeScript
ComponentProps<"button"> gives the component every native button prop with the right types (including the event type of onClick), and a union type limits variant to the styles that exist:
import type { ComponentProps, ReactNode } from "react";
type ButtonProps = ComponentProps<"button"> & {
variant?: "primary" | "secondary" | "danger";
icon?: ReactNode;
};
export default function Button({ variant = "primary", icon, children, ...rest }: ButtonProps) {
return (
<button {...rest} className={`btn btn-${variant}`}>
{icon}
{children}
</button>
);
}
export const ok = <Button type="submit" onClick={(e) => console.log(e.currentTarget.form)}>Send</Button>;
export const wrong = <Button variant="link">Read more</Button>;
tsc output:
src/Button.tsx(18,30): error TS2322: Type '"link"' is not assignable to type '"danger" | "primary" | "secondary" | undefined'.
More React component tutorials:
- Pass props to a component in React
- Create a cancel button in React
- Handle events in React
- Reuse React components
Frequently asked questions
How do I create a button component in React?
Write a function that returns a <button>: function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; }.
How do I make a reusable button component?
Accept props such as variant, size and loading, and spread the remaining props onto the <button> with {...rest}.
Why does my button submit the form?
Buttons inside a form default to type="submit". Add type="button" to buttons that should only run onClick.
How do I pass a value to onClick?
Use an arrow function: onClick={() => choose(plan)}.
How do I disable a button in React?
Pass the disabled prop: <button disabled={isSaving}>.
How do I type a button component in TypeScript?
Use ComponentProps<"button"> for the native props and add your own, e.g. variant?: "primary" | "secondary".
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn