$npx -y skills add dylantarre/design-system-skills --skill angularBuilds token-driven Angular components with signals and standalone components. Use when creating Angular component libraries, integrating design tokens, or building design system components with new control flow syntax.
| 1 | # Angular Component Patterns |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Build accessible, token-driven Angular components using modern patterns including signals, standalone components, and the new control flow syntax. Covers component architecture, TypeScript integration, services, and consuming design tokens. |
| 6 | |
| 7 | ## When to Use |
| 8 | |
| 9 | - Creating an Angular component library |
| 10 | - Building components that use design tokens |
| 11 | - Setting up a design system in Angular |
| 12 | - Converting designs to Angular components |
| 13 | |
| 14 | ## The Process |
| 15 | |
| 16 | 1. **Identify component type**: Primitive, composite, or layout? |
| 17 | 2. **Choose styling approach**: Component styles, CSS Modules, or Tailwind? |
| 18 | 3. **Define inputs/outputs**: Use signal-based inputs with transforms |
| 19 | 4. **Implement with tokens**: CSS custom properties via styles or host bindings |
| 20 | 5. **Add accessibility**: ARIA, keyboard handling, focus management |
| 21 | 6. **Create services**: Extract shared state and logic |
| 22 | |
| 23 | ## Angular 17+ Features Quick Reference |
| 24 | |
| 25 | | Feature | Purpose | Example | |
| 26 | |---------|---------|---------| |
| 27 | | `input()` | Signal-based input | `variant = input<'primary'>('primary')` | |
| 28 | | `output()` | Event emitter | `clicked = output<void>()` | |
| 29 | | `model()` | Two-way binding | `value = model<string>('')` | |
| 30 | | `computed()` | Derived signals | `isDisabled = computed(() => ...)` | |
| 31 | | `@if/@for` | Control flow | `@if (loading) { ... }` | |
| 32 | | Standalone | No NgModule needed | `standalone: true` | |
| 33 | |
| 34 | ## Project Structure |
| 35 | |
| 36 | ``` |
| 37 | src/ |
| 38 | ├── lib/ |
| 39 | │ ├── components/ |
| 40 | │ │ ├── primitives/ |
| 41 | │ │ │ ├── button/ |
| 42 | │ │ │ │ ├── button.component.ts |
| 43 | │ │ │ │ ├── button.component.html |
| 44 | │ │ │ │ ├── button.component.css |
| 45 | │ │ │ │ ├── button.component.spec.ts |
| 46 | │ │ │ │ └── index.ts |
| 47 | │ │ │ ├── input/ |
| 48 | │ │ │ └── text/ |
| 49 | │ │ ├── composite/ |
| 50 | │ │ │ ├── card/ |
| 51 | │ │ │ ├── modal/ |
| 52 | │ │ │ └── dropdown/ |
| 53 | │ │ └── layout/ |
| 54 | │ │ ├── stack/ |
| 55 | │ │ ├── grid/ |
| 56 | │ │ └── container/ |
| 57 | │ ├── services/ |
| 58 | │ │ └── theme.service.ts |
| 59 | │ ├── directives/ |
| 60 | │ │ └── focus-trap.directive.ts |
| 61 | │ ├── tokens/ |
| 62 | │ │ └── tokens.css |
| 63 | │ └── index.ts |
| 64 | └── public-api.ts |
| 65 | ``` |
| 66 | |
| 67 | ## Component Patterns |
| 68 | |
| 69 | ### Button Component |
| 70 | |
| 71 | **button.component.ts:** |
| 72 | ```typescript |
| 73 | import { |
| 74 | Component, |
| 75 | computed, |
| 76 | input, |
| 77 | output, |
| 78 | ChangeDetectionStrategy, |
| 79 | } from '@angular/core'; |
| 80 | import { NgClass } from '@angular/common'; |
| 81 | |
| 82 | export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'; |
| 83 | export type ButtonSize = 'sm' | 'md' | 'lg'; |
| 84 | |
| 85 | @Component({ |
| 86 | selector: 'ui-button', |
| 87 | standalone: true, |
| 88 | imports: [NgClass], |
| 89 | templateUrl: './button.component.html', |
| 90 | styleUrl: './button.component.css', |
| 91 | changeDetection: ChangeDetectionStrategy.OnPush, |
| 92 | host: { |
| 93 | '[class.btn--full-width]': 'fullWidth()', |
| 94 | '[attr.data-loading]': 'loading()', |
| 95 | }, |
| 96 | }) |
| 97 | export class ButtonComponent { |
| 98 | /** Visual style variant */ |
| 99 | variant = input<ButtonVariant>('primary'); |
| 100 | |
| 101 | /** Size of the button */ |
| 102 | size = input<ButtonSize>('md'); |
| 103 | |
| 104 | /** Full width button */ |
| 105 | fullWidth = input<boolean>(false); |
| 106 | |
| 107 | /** Loading state */ |
| 108 | loading = input<boolean>(false); |
| 109 | |
| 110 | /** Disabled state */ |
| 111 | disabled = input<boolean>(false); |
| 112 | |
| 113 | /** Button type */ |
| 114 | type = input<'button' | 'submit' | 'reset'>('button'); |
| 115 | |
| 116 | /** Click event */ |
| 117 | clicked = output<MouseEvent>(); |
| 118 | |
| 119 | /** Computed disabled state */ |
| 120 | isDisabled = computed(() => this.disabled() || this.loading()); |
| 121 | |
| 122 | /** CSS classes */ |
| 123 | classes = computed(() => ({ |
| 124 | btn: true, |
| 125 | [`btn--${this.variant()}`]: true, |
| 126 | [`btn--${this.size()}`]: true, |
| 127 | 'btn--loading': this.loading(), |
| 128 | })); |
| 129 | |
| 130 | handleClick(event: MouseEvent): void { |
| 131 | if (!this.isDisabled()) { |
| 132 | this.clicked.emit(event); |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | ``` |
| 137 | |
| 138 | **button.component.html:** |
| 139 | ```html |
| 140 | <button |
| 141 | [ngClass]="classes()" |
| 142 | [type]="type()" |
| 143 | [disabled]="isDisabled()" |
| 144 | [attr.aria-busy]="loading()" |
| 145 | (click)="handleClick($event)" |
| 146 | > |
| 147 | @if (loading()) { |
| 148 | <span class="btn__spinner" aria-hidden="true"></span> |
| 149 | } |
| 150 | |
| 151 | <ng-content select="[leftIcon]" /> |
| 152 | |
| 153 | <span class="btn__label"> |
| 154 | <ng-content /> |
| 155 | </span> |
| 156 | |
| 157 | <ng-content select="[rightIcon]" /> |
| 158 | </button> |
| 159 | ``` |
| 160 | |
| 161 | **button.component.css:** |
| 162 | ```css |
| 163 | :host { |
| 164 | display: inline-block; |
| 165 | } |
| 166 | |
| 167 | :host(.btn--full-width) { |
| 168 | display: block; |
| 169 | width: 100%; |
| 170 | } |
| 171 | |
| 172 | .btn { |
| 173 | display: inline-flex; |
| 174 | align-items: center; |
| 175 | justify-content: center; |
| 176 | gap: var(--spacing-xs); |
| 177 | width: 100%; |
| 178 | font-family: inherit; |
| 179 | font-weight: 500; |
| 180 | line-height: 1; |
| 181 | white-space: nowrap; |
| 182 | cursor: pointer; |
| 183 | user-select: none; |
| 184 | border: 1px solid transparent; |
| 185 | border-radius: var(--radius-md); |
| 186 | transition: |
| 187 | background-color 150ms ease, |
| 188 | border-color 150ms ease, |
| 189 | transform 100ms ease; |
| 190 | } |
| 191 | |
| 192 | .btn:focus-visible { |
| 193 | outline: 2px solid var(--color-primary-500); |
| 194 | outline-offset: 2px; |
| 195 | } |
| 196 | |
| 197 | .btn:active:not(:dis |