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.
<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>
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 {
text = input<string>('Submit');
loading = input<boolean>(false);
disabled = input<boolean>(false);
type = input<ButtonType>('submit');
variant = input<ButtonVariant>('primary');
size = input<ButtonSize>('md');
fullWidth = input<boolean>(true);
loadingText = input<string>('');
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>
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
<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)
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
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 (Default)
<app-submit-button>
Submit
</app-submit-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 disabled
loading === true → button disabled
Click is not emitted in both cases
<form (ngSubmit)="submit()">
<app-submit-button>
Submit Form
</app-submit-button>
</form>
<app-submit-button type="reset" variant="outline">
Reset
</app-submit-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
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)
Recommended Best Practices
Always bind loading to API calls
Avoid handling (click) directly – use (clicked)
Use danger variant only for destructive actions
Prefer content projection for dynamic labels
Keep business logic in parent components
Summary
This button component acts as a centralized UI control point ensuring:
It is production-ready and well-suited for use as a design-system standard button across your Angular applications.