# Angular 21+ Reusable Button Component – Complete Usage Guide with Signals, Variants & Loading States

A detailed usage guide for a global reusable button component built with Angular 21+, Signals API, and Tailwind CSS. Learn how to control variants, sizes, loading states, accessibility, and dynamic behavior from parent components with real-world examples.

### `submit-button.html`

```typescript
<button [type]="type()" [disabled]="isDisabled" [class]="buttonClasses" (click)="onClick()" [attr.aria-busy]="loading()"
    [attr.aria-disabled]="isDisabled">
    @if (loading()) {
    <svg class="w-5 h-5 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
        aria-hidden="true">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor"
            d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
        </path>
    </svg>
    <span>{{ displayText }}</span>
    } @else {
    <ng-content>{{ text() }}</ng-content>
    }
</button>
```

### `submit-button.ts`

```typescript
import { Component, ChangeDetectionStrategy, input, output } from '@angular/core';

export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export type ButtonType = 'button' | 'submit' | 'reset';

@Component({
    selector: 'app-submit-button',
    templateUrl: './submit-button.html',
    styleUrl: './submit-button.css',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SubmitButton {
    /** Button text - can also use content projection */
    text = input<string>('Submit');

    /** Loading state - shows spinner and disables button */
    loading = input<boolean>(false);

    /** Disabled state */
    disabled = input<boolean>(false);

    /** Button type */
    type = input<ButtonType>('submit');

    /** Button variant for styling */
    variant = input<ButtonVariant>('primary');

    /** Button size */
    size = input<ButtonSize>('md');

    /** Full width button */
    fullWidth = input<boolean>(true);

    /** Loading text - shown when loading */
    loadingText = input<string>('');

    /** Click event - only emitted when not loading/disabled */
    clicked = output<void>();

    protected get buttonClasses(): string {
        const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg focus:ring-4 focus:outline-none transition-colors duration-200';

        const sizeClasses: Record<ButtonSize, string> = {
            sm: 'px-3 py-2 text-xs',
            md: 'px-5 py-2.5 text-sm',
            lg: 'px-6 py-3 text-base',
        };

        const variantClasses: Record<ButtonVariant, string> = {
            primary: 'text-white bg-primary-600 hover:bg-primary-700 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800',
            secondary: 'text-white bg-secondary-600 hover:bg-secondary-700 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800',
            outline: 'text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700',
            danger: 'text-white bg-red-600 hover:bg-red-700 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800',
        };

        const widthClass = this.fullWidth() ? 'w-full' : '';
        const disabledClass = (this.loading() || this.disabled()) ? 'opacity-70 cursor-not-allowed' : '';

        return `${baseClasses} ${sizeClasses[this.size()]} ${variantClasses[this.variant()]} ${widthClass} ${disabledClass}`.trim();
    }

    protected get isDisabled(): boolean {
        return this.loading() || this.disabled();
    }

    protected get displayText(): string {
        if (this.loading() && this.loadingText()) {
            return this.loadingText();
        }
        return this.text();
    }

    protected onClick(): void {
        if (!this.isDisabled) {
            this.clicked.emit();
        }
    }
}
```

# Usage Guide

## Overview

`SubmitButton` is a **reusable, signal-based Angular button component** designed for forms and general actions.  
It supports:

* Variant-based styling (primary, secondary, outline, danger)
    
* Size control
    
* Loading and disabled states
    
* Full-width or inline layout
    
* Accessibility (ARIA attributes)
    
* Controlled click emission (blocked during loading/disabled)
    

This component is suitable for **form submissions, API actions, and workflow triggers** across the application.

---

## Component Selector

```xml
<app-submit-button></app-submit-button>
```

---

## Public API (Inputs & Output)

### Inputs (Signals)

All inputs are **signal-based**, meaning values can be **static or dynamically bound** from the parent component.

| Input Name | Type | Default | Purpose |
| --- | --- | --- | --- |
| `text` | `string` | `'Submit'` | Button label (fallback when no content projection) |
| `loading` | `boolean` | `false` | Shows spinner, disables button |
| `disabled` | `boolean` | `false` | Disables the button |
| `type` | `'button' | 'submit' | 'reset'` | `'submit'` | Native HTML button type |
| `variant` | `'primary' | 'secondary' | 'outline' | 'danger'` | `'primary'` | Visual style |
| `size` | `'sm' | 'md' | 'lg'` | `'md'` | Button size |
| `fullWidth` | `boolean` | `true` | Controls width (`w-full`) |
| `loadingText` | `string` | `''` | Text displayed during loading |

---

### Output

| Output Name | Type | When Emitted |
| --- | --- | --- |
| `clicked` | `EventEmitter<void>` | Only when `loading === false` AND `disabled === false` |

---

## Basic Usage Example

### Simple Submit Button

```xml
<app-submit-button></app-submit-button>
```

**Result**

* Text: `Submit`
    
* Variant: Primary
    
* Size: Medium
    
* Full width
    
* Type: submit
    

---

## Passing Dynamic Data from Parent Component

### Parent Component (TypeScript)

```typescript
export class LoginComponent {
  isSubmitting = false;

  onSubmit() {
    this.isSubmitting = true;

    setTimeout(() => {
      this.isSubmitting = false;
    }, 2000);
  }
}
```

### Template (HTML)

```xml
<app-submit-button
  text="Login"
  [loading]="isSubmitting"
  loadingText="Logging in..."
  (clicked)="onSubmit()">
</app-submit-button>
```

### What Is Controlled Dynamically?

| Property | Controlled By Parent |
| --- | --- |
| Button label | `text` |
| Spinner visibility | `loading` |
| Disabled state | Auto (via `loading`) |
| Click action | `(clicked)` |

---

## Content Projection (Custom Button Text)

You can override the `text` input using **content projection**.

```xml
<app-submit-button variant="secondary">
  Save Changes
</app-submit-button>
```

### Rule

* If `loading === true` → projected content is hidden
    
* If `loading === false` → projected content is shown
    

---

## Variants (Styling Control)

```xml
<app-submit-button variant="primary">Primary</app-submit-button>
<app-submit-button variant="secondary">Secondary</app-submit-button>
<app-submit-button variant="outline">Outline</app-submit-button>
<app-submit-button variant="danger">Delete</app-submit-button>
```

### Use Case Guidance

| Variant | Recommended Usage |
| --- | --- |
| `primary` | Main call-to-action |
| `secondary` | Secondary actions |
| `outline` | Low-priority actions |
| `danger` | Destructive actions |

---

## Size Control

```xml
<app-submit-button size="sm">Small</app-submit-button>
<app-submit-button size="md">Medium</app-submit-button>
<app-submit-button size="lg">Large</app-submit-button>
```

---

## Full Width vs Inline Button

### Full Width (Default)

```xml
<app-submit-button>
  Submit
</app-submit-button>
```

### Inline Button

```xml
<app-submit-button [fullWidth]="false">
  Cancel
</app-submit-button>
```

---

## Disabled State Control

### Manual Disable

```xml
<app-submit-button
  text="Save"
  [disabled]="true">
</app-submit-button>
```

### Combined Disable Logic (Recommended)

```xml
<app-submit-button
  text="Save"
  [disabled]="!form.valid"
  [loading]="isSaving"
  (clicked)="save()">
</app-submit-button>
```

### Internal Behavior

* `disabled === true` → button disabled
    
* `loading === true` → button disabled
    
* Click is **not emitted** in both cases
    

---

## Button Type (Forms)

### Submit Button (Default)

```xml
<form (ngSubmit)="submit()">
  <app-submit-button>
    Submit Form
  </app-submit-button>
</form>
```

### Reset Button

```xml
<app-submit-button type="reset" variant="outline">
  Reset
</app-submit-button>
```

### Normal Button

```xml
<app-submit-button
  type="button"
  (clicked)="openModal()">
  Open Modal
</app-submit-button>
```

---

## Loading State UX

```xml
<app-submit-button
  text="Create Account"
  loadingText="Creating..."
  [loading]="isCreating"
  (clicked)="createAccount()">
</app-submit-button>
```

### Behavior

* Spinner shown
    
* Text replaced by `loadingText`
    
* Click blocked
    
* `aria-busy="true"` applied
    

---

## Accessibility Features

Built-in accessibility support:

* `aria-busy` → reflects loading state
    
* `aria-disabled` → reflects disabled state
    
* Button is natively disabled (`disabled` attribute)
    
* Spinner marked `aria-hidden="true"`
    

No additional ARIA configuration required from parent.

---

## What Parent Can Control vs Internal Logic

### Controlled by Parent Component

* Button label (`text` or projected content)
    
* Loading state (`loading`)
    
* Disabled logic (`disabled`)
    
* Visual style (`variant`, `size`)
    
* Layout (`fullWidth`)
    
* Button behavior (`type`)
    
* Action handling (`clicked`)
    

### Controlled Internally by Button

* Spinner visibility
    
* Preventing double clicks
    
* Disabled logic during loading
    
* CSS class composition
    
* Accessibility attributes
    

---

## Recommended Best Practices

1. **Always bind** `loading` to API calls
    
2. **Avoid handling** `(click)` directly – use `(clicked)`
    
3. **Use** `danger` variant only for destructive actions
    
4. **Prefer content projection for dynamic labels**
    
5. **Keep business logic in parent components**
    

---

## Summary

This button component acts as a **centralized UI control point** ensuring:

* Consistent UX
    
* Safe click handling
    
* Accessible behavior
    
* Tailwind-based theming
    
* Angular 21+ signal best practices
    

It is production-ready and well-suited for use as a **design-system standard button** across your Angular applications.
