
Angular Ui Patterns
- 603 installs
- 44.4k repo stars
- Updated August 4, 2026
- sickn33/antigravity-awesome-skills
angular-ui-patterns is a Claude Code skill that teaches modern Angular loading, error, async data, and @defer patterns for developers who need list and detail UIs that never show stale or silent failure states.
About
angular-ui-patterns is a frontend skill from sickn33/antigravity-awesome-skills that codifies five core UI principles: never show stale data, always surface errors, optimistic updates, progressive @defer disclosure, and graceful degradation with partial data. It provides TypeScript and template examples for @if/@else loading gates, error-state components with retry handlers, and deferred block loading so indicators appear only when no cached data exists. Developers reach for angular-ui-patterns when building Angular dashboards, master-detail views, or async feeds where blank screens, flickering spinners, or hidden API failures would hurt usability. The skill is marked safe-risk and targets Angular projects using modern control-flow syntax and component signals.
- 5 core principles: no stale UI, surface errors, optimistic updates, progressive disclosure with @defer, graceful degrada
- Golden rule: show loading only when loading AND there is no data to display
- Template decision tree: error → empty → loading-without-data → content list
- Signal/store injection patterns with inject(ItemStore) for items, loading, and error
- Risk marked safe in skill metadata for standard UI refactors
Angular Ui Patterns by the numbers
- 603 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #562 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill angular-ui-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 603 |
|---|---|
| repo stars | ★ 44.4k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | sickn33/antigravity-awesome-skills ↗ |
How do you handle loading and errors in Angular?
Apply modern Angular patterns for loading, errors, async data, and @defer so list and detail UIs never show stale or silent failure states.
Who is it for?
Angular developers building data-heavy list or detail UIs who want consistent async, error, and deferred-render behavior across components.
Skip if: Teams on legacy Angular without control-flow syntax or projects that need routing, forms, or state-management architecture rather than display-state patterns.
When should I use this skill?
A developer builds Angular components with async data, loading spinners, error handling, @defer blocks, or stale UI complaints.
What you get
Angular components with correct loading gates, surfaced error states with retry, optimistic updates, and @defer progressive disclosure.
- Angular component templates
- Error and loading state patterns
By the numbers
- Documents 5 core UI principles for loading, errors, and async data display
Files
Angular UI Patterns
Core Principles
1. Never show stale UI - Loading states only when actually loading 2. Always surface errors - Users must know when something fails 3. Optimistic updates - Make the UI feel instant 4. Progressive disclosure - Use @defer to show content as available 5. Graceful degradation - Partial data is better than no data
---
Loading State Patterns
The Golden Rule
Show loading indicator ONLY when there's no data to display.
@Component({
template: `
@if (error()) {
<app-error-state [error]="error()" (retry)="load()" />
} @else if (loading() && !items().length) {
<app-skeleton-list />
} @else if (!items().length) {
<app-empty-state message="No items found" />
} @else {
<app-item-list [items]="items()" />
}
`,
})
export class ItemListComponent {
private store = inject(ItemStore);
items = this.store.items;
loading = this.store.loading;
error = this.store.error;
}Loading State Decision Tree
Is there an error?
→ Yes: Show error state with retry option
→ No: Continue
Is it loading AND we have no data?
→ Yes: Show loading indicator (spinner/skeleton)
→ No: Continue
Do we have data?
→ Yes, with items: Show the data
→ Yes, but empty: Show empty state
→ No: Show loading (fallback)Skeleton vs Spinner
| Use Skeleton When | Use Spinner When |
|---|---|
| Known content shape | Unknown content shape |
| List/card layouts | Modal actions |
| Initial page load | Button submissions |
| Content placeholders | Inline operations |
---
Control Flow Patterns
@if/@else for Conditional Rendering
@if (user(); as user) {
<span>Welcome, {{ user.name }}</span>
} @else if (loading()) {
<app-spinner size="small" />
} @else {
<a routerLink="/login">Sign In</a>
}@for with Track
@for (item of items(); track item.id) {
<app-item-card [item]="item" (delete)="remove(item.id)" />
} @empty {
<app-empty-state
icon="inbox"
message="No items yet"
actionLabel="Create Item"
(action)="create()"
/>
}@defer for Progressive Loading
<!-- Critical content loads immediately -->
<app-header />
<app-hero-section />
<!-- Non-critical content deferred -->
@defer (on viewport) {
<app-comments [postId]="postId()" />
} @placeholder {
<div class="h-32 bg-gray-100 animate-pulse"></div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<app-error-state message="Failed to load comments" />
}---
Error Handling Patterns
Error Handling Hierarchy
1. Inline error (field-level) → Form validation errors
2. Toast notification → Recoverable errors, user can retry
3. Error banner → Page-level errors, data still partially usable
4. Full error screen → Unrecoverable, needs user actionAlways Show Errors
CRITICAL: Never swallow errors silently.
// CORRECT - Error always surfaced to user
@Component({...})
export class CreateItemComponent {
private store = inject(ItemStore);
private toast = inject(ToastService);
async create(data: CreateItemDto) {
try {
await this.store.create(data);
this.toast.success('Item created successfully');
this.router.navigate(['/items']);
} catch (error) {
console.error('createItem failed:', error);
this.toast.error('Failed to create item. Please try again.');
}
}
}
// WRONG - Error silently caught
async create(data: CreateItemDto) {
try {
await this.store.create(data);
} catch (error) {
console.error(error); // User sees nothing!
}
}Error State Component Pattern
@Component({
selector: "app-error-state",
standalone: true,
imports: [NgOptimizedImage],
template: `
<div class="error-state">
<img ngSrc="/assets/error-icon.svg" width="64" height="64" alt="" />
<h3>{{ title() }}</h3>
<p>{{ message() }}</p>
@if (retry.observed) {
<button (click)="retry.emit()" class="btn-primary">Try Again</button>
}
</div>
`,
})
export class ErrorStateComponent {
title = input("Something went wrong");
message = input("An unexpected error occurred");
retry = output<void>();
}---
Button State Patterns
Button Loading State
<button
(click)="handleSubmit()"
[disabled]="isSubmitting() || !form.valid"
class="btn-primary"
>
@if (isSubmitting()) {
<app-spinner size="small" class="mr-2" />
Saving... } @else { Save Changes }
</button>Disable During Operations
CRITICAL: Always disable triggers during async operations.
// CORRECT - Button disabled while loading
@Component({
template: `
<button
[disabled]="saving()"
(click)="save()"
>
@if (saving()) {
<app-spinner size="sm" /> Saving...
} @else {
Save
}
</button>
`
})
export class SaveButtonComponent {
saving = signal(false);
async save() {
this.saving.set(true);
try {
await this.service.save();
} finally {
this.saving.set(false);
}
}
}
// WRONG - User can click multiple times
<button (click)="save()">
{{ saving() ? 'Saving...' : 'Save' }}
</button>---
Empty States
Empty State Requirements
Every list/collection MUST have an empty state:
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
} @empty {
<app-empty-state
icon="folder-open"
title="No items yet"
description="Create your first item to get started"
actionLabel="Create Item"
(action)="openCreateDialog()"
/>
}Contextual Empty States
@Component({
selector: "app-empty-state",
template: `
<div class="empty-state">
<span class="icon" [class]="icon()"></span>
<h3>{{ title() }}</h3>
<p>{{ description() }}</p>
@if (actionLabel()) {
<button (click)="action.emit()" class="btn-primary">
{{ actionLabel() }}
</button>
}
</div>
`,
})
export class EmptyStateComponent {
icon = input("inbox");
title = input.required<string>();
description = input("");
actionLabel = input<string | null>(null);
action = output<void>();
}---
Form Patterns
Form with Loading and Validation
@Component({
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-field">
<label for="name">Name</label>
<input
id="name"
formControlName="name"
[class.error]="isFieldInvalid('name')"
/>
@if (isFieldInvalid("name")) {
<span class="error-text">
{{ getFieldError("name") }}
</span>
}
</div>
<div class="form-field">
<label for="email">Email</label>
<input id="email" type="email" formControlName="email" />
@if (isFieldInvalid("email")) {
<span class="error-text">
{{ getFieldError("email") }}
</span>
}
</div>
<button type="submit" [disabled]="form.invalid || submitting()">
@if (submitting()) {
<app-spinner size="sm" /> Submitting...
} @else {
Submit
}
</button>
</form>
`,
})
export class UserFormComponent {
private fb = inject(FormBuilder);
submitting = signal(false);
form = this.fb.group({
name: ["", [Validators.required, Validators.minLength(2)]],
email: ["", [Validators.required, Validators.email]],
});
isFieldInvalid(field: string): boolean {
const control = this.form.get(field);
return control ? control.invalid && control.touched : false;
}
getFieldError(field: string): string {
const control = this.form.get(field);
if (control?.hasError("required")) return "This field is required";
if (control?.hasError("email")) return "Invalid email format";
if (control?.hasError("minlength")) return "Too short";
return "";
}
async onSubmit() {
if (this.form.invalid) return;
this.submitting.set(true);
try {
await this.service.submit(this.form.value);
this.toast.success("Submitted successfully");
} catch {
this.toast.error("Submission failed");
} finally {
this.submitting.set(false);
}
}
}---
Dialog/Modal Patterns
Confirmation Dialog
// dialog.service.ts
@Injectable({ providedIn: 'root' })
export class DialogService {
private dialog = inject(Dialog); // CDK Dialog or custom
async confirm(options: {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
}): Promise<boolean> {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: options,
});
return await firstValueFrom(dialogRef.closed) ?? false;
}
}
// Usage
async deleteItem(item: Item) {
const confirmed = await this.dialog.confirm({
title: 'Delete Item',
message: `Are you sure you want to delete "${item.name}"?`,
confirmText: 'Delete',
});
if (confirmed) {
await this.store.delete(item.id);
}
}---
Anti-Patterns
Loading States
// WRONG - Spinner when data exists (causes flash on refetch)
@if (loading()) {
<app-spinner />
}
// CORRECT - Only show loading without data
@if (loading() && !items().length) {
<app-spinner />
}Error Handling
// WRONG - Error swallowed
try {
await this.service.save();
} catch (e) {
console.log(e); // User has no idea!
}
// CORRECT - Error surfaced
try {
await this.service.save();
} catch (e) {
console.error("Save failed:", e);
this.toast.error("Failed to save. Please try again.");
}Button States
<!-- WRONG - Button not disabled during submission -->
<button (click)="submit()">Submit</button>
<!-- CORRECT - Disabled and shows loading -->
<button (click)="submit()" [disabled]="loading()">
@if (loading()) {
<app-spinner size="sm" />
} Submit
</button>---
UI State Checklist
Before completing any UI component:
UI States
- [ ] Error state handled and shown to user
- [ ] Loading state shown only when no data exists
- [ ] Empty state provided for collections (
@emptyblock) - [ ] Buttons disabled during async operations
- [ ] Buttons show loading indicator when appropriate
Data & Mutations
- [ ] All async operations have error handling
- [ ] All user actions have feedback (toast/visual)
- [ ] Optimistic updates rollback on failure
Accessibility
- [ ] Loading states announced to screen readers
- [ ] Error messages linked to form fields
- [ ] Focus management after state changes
---
Integration with Other Skills
- angular-state-management: Use Signal stores for state
- angular: Apply modern patterns (Signals, @defer)
- testing-patterns: Test all UI states
When to Use
This skill is applicable to execute the workflow or actions described in the overview.
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
{
"version": "1.0.0",
"organization": "Antigravity Awesome Skills",
"date": "February 2026",
"abstract": "Modern UI patterns for Angular applications designed for AI agents and LLMs. Covers loading states, error handling, progressive disclosure, and data display patterns. Emphasizes showing loading only without data, surfacing all errors, optimistic updates, and graceful degradation using @defer. Includes decision trees and anti-patterns to avoid.",
"references": [
"https://angular.dev/guide/defer",
"https://angular.dev/guide/templates",
"https://material.angular.io",
"https://ng-spartan.com"
]
}
Angular UI Patterns
Modern UI patterns for building robust Angular applications optimized for AI agents and LLMs.
Overview
This skill covers essential UI patterns for:
- Loading States - Skeleton vs spinner decision trees
- Error Handling - Error boundary hierarchy and recovery
- Progressive Disclosure - Using
@deferfor lazy rendering - Data Display - Handling empty, loading, and error states
- Form Patterns - Submission states and validation feedback
- Dialog/Modal Patterns - Proper dialog lifecycle management
Core Principles
1. Never show stale UI - Only show loading when no data exists 2. Surface all errors - Never silently fail 3. Optimistic updates - Update UI before server confirms 4. Progressive disclosure - Use @defer to load non-critical content 5. Graceful degradation - Fallback for failed features
Structure
The SKILL.md file includes:
1. Golden Rules - Non-negotiable patterns to follow 2. Decision Trees - When to use skeleton vs spinner 3. Code Examples - Correct vs incorrect implementations 4. Anti-patterns - Common mistakes to avoid
Quick Reference
<!-- Angular template pattern for data states -->
@if (error()) {
<app-error-state [error]="error()" (retry)="load()" />
} @else if (loading() && !data()) {
<app-skeleton-state />
} @else if (!data()?.length) {
<app-empty-state message="No items found" />
} @else {
<app-data-display [data]="data()" />
}Version
Current version: 1.0.0 (February 2026)
References
Related skills
How it compares
Use angular-ui-patterns for in-component display-state rules; pair with routing or NgRx skills when the problem is navigation or global store design.
FAQ
When should Angular show a loading spinner?
angular-ui-patterns says show a loading indicator only when there is no data to display; if cached or partial data exists, render it and avoid flickering stale spinners over populated content.
What Angular features does angular-ui-patterns cover?
angular-ui-patterns covers modern control-flow @if/@else, @defer blocks, signal-driven state, optimistic updates, error-state components with retry, and graceful degradation when APIs return partial data.
Does angular-ui-patterns help with silent API failures?
Yes—angular-ui-patterns requires always surfacing errors so users see failure states and retry actions instead of blank or frozen list and detail screens after async calls fail.
Is Angular Ui Patterns safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.