Angular 21+ Reusable Button Component – Complete Usage Guide with Signals, Variants & Loading States
Technology Enthusiast and voracious reader with a demonstrated history of working in the computer software industry. Skilled in PHP, JavaScript, NodeJS, Angular, MySQL, MongoDB, Web3, Product Development, Project Management, and Teamwork.
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
<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
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
<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
<app-submit-button></app-submit-button>
Result
Text:
SubmitVariant: Primary
Size: Medium
Full width
Type: submit
Passing Dynamic Data from Parent Component
Parent Component (TypeScript)
export class LoginComponent {
isSubmitting = false;
onSubmit() {
this.isSubmitting = true;
setTimeout(() => {
this.isSubmitting = false;
}, 2000);
}
}
Template (HTML)
<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.
<app-submit-button variant="secondary">
Save Changes
</app-submit-button>
Rule
If
loading === true→ projected content is hiddenIf
loading === false→ projected content is shown
Variants (Styling Control)
<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
<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)
<app-submit-button>
Submit
</app-submit-button>
Inline Button
<app-submit-button [fullWidth]="false">
Cancel
</app-submit-button>
Disabled State Control
Manual Disable
<app-submit-button
text="Save"
[disabled]="true">
</app-submit-button>
Combined Disable Logic (Recommended)
<app-submit-button
text="Save"
[disabled]="!form.valid"
[loading]="isSaving"
(clicked)="save()">
</app-submit-button>
Internal Behavior
disabled === true→ button disabledloading === true→ button disabledClick is not emitted in both cases
Button Type (Forms)
Submit Button (Default)
<form (ngSubmit)="submit()">
<app-submit-button>
Submit Form
</app-submit-button>
</form>
Reset Button
<app-submit-button type="reset" variant="outline">
Reset
</app-submit-button>
Normal Button
<app-submit-button
type="button"
(clicked)="openModal()">
Open Modal
</app-submit-button>
Loading State UX
<app-submit-button
text="Create Account"
loadingText="Creating..."
[loading]="isCreating"
(clicked)="createAccount()">
</app-submit-button>
Behavior
Spinner shown
Text replaced by
loadingTextClick blocked
aria-busy="true"applied
Accessibility Features
Built-in accessibility support:
aria-busy→ reflects loading statearia-disabled→ reflects disabled stateButton is natively disabled (
disabledattribute)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 (
textor 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
Always bind
loadingto API callsAvoid handling
(click)directly – use(clicked)Use
dangervariant only for destructive actionsPrefer content projection for dynamic labels
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.