diff --git a/react/src/components/Button.test.tsx b/react/src/components/Button.test.tsx
new file mode 100644
index 0000000..6d14d53
--- /dev/null
+++ b/react/src/components/Button.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { Button } from './Button';
+
+describe('Button Component', () => {
+ it('renders children text correctly', () => {
+ render();
+ expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
+ });
+
+ it('applies custom variant classes matching tokens', () => {
+ const { rerender } = render();
+ expect(screen.getByRole('button')).toHaveClass('bg-red-600');
+
+ rerender();
+ expect(screen.getByRole('button')).toHaveClass('border-gray-300');
+ });
+
+ it('triggers onClick handler when clicked', () => {
+ const handleClick = jest.fn();
+ render();
+
+ fireEvent.click(screen.getByRole('button'));
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles loading state accessibility and disables interaction safely', () => {
+ const handleClick = jest.fn();
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button).toBeDisabled();
+ expect(button).toHaveAttribute('aria-disabled', 'true');
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
+
+ fireEvent.click(button);
+ expect(handleClick).not.toHaveBeenCalled();
+ });
+
+ it('supports passing traditional forwarding refs correctly', () => {
+ const ref = React.createRef();
+ render();
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
+ });
+});
\ No newline at end of file
diff --git a/react/src/components/Button.tsx b/react/src/components/Button.tsx
new file mode 100644
index 0000000..e77a4ed
--- /dev/null
+++ b/react/src/components/Button.tsx
@@ -0,0 +1,79 @@
+import React, { forwardRef } from 'react';
+
+export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'outline';
+export type ButtonSize = 'sm' | 'md' | 'lg';
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ isLoading?: boolean;
+}
+
+export const Button = forwardRef(
+ (
+ {
+ children,
+ variant = 'primary',
+ size = 'md',
+ isLoading = false,
+ disabled,
+ className = '',
+ type = 'button',
+ ...props
+ },
+ ref
+ ) => {
+ // Base design tokens including structural, layout, and accessible focus outlines
+ const baseStyles =
+ 'inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none';
+
+ const variants: Record = {
+ primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
+ secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
+ danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
+ outline: 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
+ };
+
+ const sizes: Record = {
+ sm: 'px-3 py-1.5 text-sm',
+ md: 'px-4 py-2 text-base',
+ lg: 'px-6 py-3 text-lg',
+ };
+
+ const isInteractionDisabled = disabled || isLoading;
+
+ return (
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
\ No newline at end of file
diff --git a/react/src/components/index.ts b/react/src/components/index.ts
index aa7c45f..bef3ff3 100644
--- a/react/src/components/index.ts
+++ b/react/src/components/index.ts
@@ -3,3 +3,9 @@ export type { AlertProps, AlertVariant } from './Alert';
export { Badge } from './Badge';
export type { BadgeProps, BadgeVariant, BadgeSize } from './Badge';
+
+export { Button } from './Button';
+export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
+
+export { Modal } from './Modal';
+export type { ModalProps } from './Modal';
\ No newline at end of file