angular
Skill50 starsChanged 11 months agoMITView on GitHub
Builds 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.
15 other projects use a version of this.
Adopted by
Most recently taken up by
- Lukk17/agent-standardstheir own version, first seen Nov 7, 2026
- gingerminds/claude-skillstheir own version, first seen Nov 1, 2026
- xfstudio/skillstheir own version, first seen Oct 13, 2026
- G1Joshi/Agent-Skillstheir own version, first seen Oct 13, 2026
- nimadorostkar/Claude-Skills-collectiontheir own version, first seen Oct 13, 2026
- kid-sid/claude-spellbooktheir own version, first seen Oct 9, 2026
Used with
Projects with it also use python.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
rogue-dev-studioSkillpython
Python 3.13 with uv package manager via mise. Use when the project needs this capability or the user / team manifest asks for it. Use for specialized `python` work when listed in TEAM.yaml or explicitly requested.
Also in 3 projects that use this
Other versions15 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 15
- Mindrally/skills★ 275same name
- kid-sid/claude-spellbook★ 190same name
- kumaran-is/claude-code-onboarding★ 35same name
- nimadorostkar/Claude-Skills-collection★ 33same name
- G1Joshi/Agent-Skills★ 15same name
- xfstudio/skills★ 9same name
- lucavb/skills★ 3same name
- ai4brands-design/claude-skills★ 2same name
- ffsshhttiikk/opencode-agents-skills★ 2same name
- gingerminds/claude-skills★ 2same name
5 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository29 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- aria-patternsSkill
- color-contrastSkill
- focus-statesSkill
- component-docsSkill
- token-docsSkill
- reactSkill
- svelteSkill
- vueSkill
- getting-startedSkill
- animation-principlesSkill
- compound-componentsSkill
- dark-modeSkill
- icon-systemSkill
- layout-primitivesSkill
- breakpointsSkill
- color-scaleSkill
- design-tokens-structureSkill
- motion-scaleSkill
- radius-scaleSkill
- responsive-typographySkill
- shadow-scaleSkill
- spacing-scaleSkill
- type-scaleSkill
- z-index-scaleSkill
- figmaSkill
- framerSkill
- storybookSkill
- style-dictionarySkill
The file
We split this skill into the rules it gives an agent
1 rule. 0 are common, 1 is distinctive, and 0 are very specific to this project.
0
Common
Most of the 14 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 23
Define inputs/outputs: Use signal-based inputs with transforms
“Common” is measured: kept by at least 6 in 10 of the 14 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Angular Component Patterns
Overview
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.
When to Use
- Creating an Angular component library
- Building components that use design tokens
- Setting up a design system in Angular
- Converting designs to Angular components
The Process
- Identify component type: Primitive, composite, or layout?
- Choose styling approach: Component styles, CSS Modules, or Tailwind?
- Define inputs/outputs: Use signal-based inputs with transforms
- Implement with tokens: CSS custom properties via styles or host bindings
- Add accessibility: ARIA, keyboard handling, focus management
- Create services: Extract shared state and logic
Angular 17+ Features Quick Reference
| Feature | Purpose | Example |
input() | Signal-based input | variant = input<'primary'>('primary') |
output() | Event emitter | clicked = output<void>() |
model() | Two-way binding | value = model<string>('') |
computed() | Derived signals | isDisabled = computed(() => ...) |
@if/@for | Control flow | @if (loading) { ... } |
| Standalone | No NgModule needed | standalone: true |
Project Structure
src/
├── lib/
│ ├── components/
│ │ ├── primitives/
│ │ │ ├── button/
│ │ │ │ ├── button.component.ts
│ │ │ │ ├── button.component.html
│ │ │ │ ├── button.component.css
│ │ │ │ ├── button.component.spec.ts
│ │ │ │ └── index.ts
│ │ │ ├── input/
│ │ │ └── text/
│ │ ├── composite/
│ │ │ ├── card/
│ │ │ ├── modal/
│ │ │ └── dropdown/
│ │ └── layout/
│ │ ├── stack/
│ │ ├── grid/
│ │ └── container/
│ ├── services/
│ │ └── theme.service.ts
│ ├── directives/
│ │ └── focus-trap.directive.ts
│ ├── tokens/
│ │ └── tokens.css
│ └── index.ts
└── public-api.tsComponent Patterns
Button Component
button.component.ts:
import {
Component,
computed,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { NgClass } from '@angular/common';
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
@Component({
selector: 'ui-button',
standalone: true,
imports: [NgClass],
templateUrl: './button.component.html',
styleUrl: './button.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.btn--full-width]': 'fullWidth()',
'[attr.data-loading]': 'loading()',
},
})
export class ButtonComponent {
/** Visual style variant */
variant = input<ButtonVariant>('primary');
/** Size of the button */
size = input<ButtonSize>('md');
/** Full width button */
fullWidth = input<boolean>(false);
/** Loading state */
loading = input<boolean>(false);
/** Disabled state */
disabled = input<boolean>(false);
/** Button type */
type = input<'button' | 'submit' | 'reset'>('button');
/** Click event */
clicked = output<MouseEvent>();
/** Computed disabled state */
isDisabled = computed(() => this.disabled() || this.loading());
/** CSS classes */
classes = computed(() => ({
btn: true,
[`btn--${this.variant()}`]: true,
[`btn--${this.size()}`]: true,
'btn--loading': this.loading(),
}));
handleClick(event: MouseEvent): void {
if (!this.isDisabled()) {
this.clicked.emit(event);
}
}
}button.component.html:
<button
[ngClass]="classes()"
[type]="type()"
[disabled]="isDisabled()"
[attr.aria-busy]="loading()"
(click)="handleClick($event)"
>
@if (loading()) {
<span class="btn__spinner" aria-hidden="true"></span>
}
<ng-content select="[leftIcon]" />
<span class="btn__label">
<ng-content />
</span>
<ng-content select="[rightIcon]" />
</button>button.component.css:
:host {
display: inline-block;
}
:host(.btn--full-width) {
display: block;
width: 100%;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
width: 100%;
font-family: inherit;
font-weight: 500;
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
border-radius: var(--radius-md);
transition:
background-color 150ms ease,
border-color 150ms ease,
transform 100ms ease;
}
.btn:focus-visible {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
.btn:active:not(:disabled) {
transform: scale(0.98);
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* Variants */
.btn--primary {
background-color: var(--color-primary-500);
color: white;
}
.btn--primary:hover:not(:disabled) {
background-color: var(--color-primary-600);
}
.btn--secondary {
background-color: transparent;
border-color: var(--color-gray-300);
color: var(--color-gray-700);
}
.btn--secondary:hover:not(:disabled) {
background-color: var(--color-gray-50);
border-color: var(--color-gray-400);
}
.btn--ghost {
background-color: transparent;
color: var(--color-gray-700);
}
.btn--ghost:hover:not(:disabled) {
background-color: var(--color-gray-100);
}
.btn--danger {
background-color: var(--color-error-500);
color: white;
}
.btn--danger:hover:not(:disabled) {
background-color: var(--color-error-600);
}
/* Sizes */
.btn--sm {
height: 32px;
padding: 0 var(--spacing-sm);
font-size: var(--text-sm);
}
.btn--md {
height: 40px;
padding: 0 var(--spacing-md);
font-size: var(--text-base);
}
.btn--lg {
height: 48px;
padding: 0 var(--spacing-lg);
font-size: var(--text-lg);
}
/* Loading */
.btn--loading .btn__label {
opacity: 0;
}
.btn__spinner {
position: absolute;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 600ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}Input Component
input.component.ts:
import {
Component,
computed,
input,
model,
signal,
output,
forwardRef,
ChangeDetectionStrategy,
ElementRef,
ViewChild,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export type InputSize = 'sm' | 'md' | 'lg';
@Component({
selector: 'ui-input',
standalone: true,
templateUrl: './input.component.html',
styleUrl: './input.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true,
},
],
host: {
'[class.input-container--full-width]': 'fullWidth()',
},
})
export class InputComponent implements ControlValueAccessor {
@ViewChild('inputRef') inputRef!: ElementRef<HTMLInputElement>;
/** Bound value */
value = model<string>('');
/** Label text */
label = input<string>('');
/** Placeholder */
placeholder = input<string>('');
/** Helper text */
helperText = input<string>('');
/** Error message */
error = input<string>('');
/** Input type */
type = input<'text' | 'email' | 'password' | 'number' | 'tel'>('text');
/** Size variant */
size = input<InputSize>('md');
/** Disabled */
disabled = input<boolean>(false);
/** Required */
required = input<boolean>(false);
/** Full width */
fullWidth = input<boolean>(false);
/** Input ID */
inputId = input<string>(`input-${crypto.randomUUID().slice(0, 8)}`);
/** Blur event */
blurred = output<FocusEvent>();
/** Focus event */
focused = output<FocusEvent>();
/** CVA callbacks */
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
/** Computed IDs */
helperId = computed(() => `${this.inputId()}-helper`);
errorId = computed(() => `${this.inputId()}-error`);
/** Wrapper classes */
wrapperClasses = computed(() => ({
'input-wrapper': true,
[`input-wrapper--${this.size()}`]: true,
'input-wrapper--error': !!this.error(),
'input-wrapper--disabled': this.disabled(),
}));
/** Focus the input */
focus(): void {
this.inputRef?.nativeElement?.focus();
}
/** Handle input changes */
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.value.set(target.value);
this.onChange(target.value);
}
/** Handle blur */
onBlur(event: FocusEvent): void {
this.onTouched();
this.blurred.emit(event);
}
/** ControlValueAccessor */
writeValue(value: string): void {
this.value.set(value ?? '');
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// Handle via input signal in template
}
}input.component.html:
<div class="input-container">
@if (label()) {
<label [for]="inputId()" class="input-label">
{{ label() }}
@if (required()) {
<span class="input-required" aria-hidden="true">*</span>
}
</label>
}
<div [ngClass]="wrapperClasses()">
<span class="input-adornment">
<ng-content select="[startAdornment]" />
</span>
<input
#inputRef
[id]="inputId()"
[type]="type()"
[value]="value()"
[placeholder]="placeholder()"
[disabled]="disabled()"
[required]="required()"
[attr.aria-invalid]="!!error()"
[attr.aria-describedby]="error() ? errorId() : helperText() ? helperId() : null"
class="input"
(input)="onInput($event)"
(blur)="onBlur($event)"
(focus)="focused.emit($event)"
/>
<span class="input-adornment">
<ng-content select="[endAdornment]" />
</span>
</div>
@if (error()) {
<span [id]="errorId()" class="input-error" role="alert">
{{ error() }}
</span>
} @else if (helperText()) {
<span [id]="helperId()" class="input-helper">
{{ helperText() }}
</span>
}
</div>Stack Layout Component
stack.component.ts:
import {
Component,
input,
computed,
ChangeDetectionStrategy,
} from '@angular/core';
import { NgStyle, NgClass } from '@angular/common';
export type SpacingToken = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
export type AlignOption = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
export type JustifyOption = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
@Component({
selector: 'ui-stack',
standalone: true,
imports: [NgStyle, NgClass],
template: `
<ng-content />
`,
styles: [`
:host {
display: flex;
gap: var(--stack-gap, var(--spacing-md));
align-items: var(--stack-align, stretch);
justify-content: var(--stack-justify, flex-start);
}
:host(.stack--column) {
flex-direction: column;
}
:host(.stack--row) {
flex-direction: row;
}
:host(.stack--wrap) {
flex-wrap: wrap;
}
:host(.stack--full-width) {
width: 100%;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.stack--column]': "direction() === 'column'",
'[class.stack--row]': "direction() === 'row'",
'[class.stack--wrap]': 'wrap()',
'[class.stack--full-width]': 'fullWidth()',
'[style.--stack-gap]': "'var(--spacing-' + gap() + ')'",
'[style.--stack-align]': 'alignValue()',
'[style.--stack-justify]': 'justifyValue()',
},
})
export class StackComponent {
direction = input<'row' | 'column'>('column');
gap = input<SpacingToken>('md');
align = input<AlignOption>('stretch');
justify = input<JustifyOption>('start');
wrap = input<boolean>(false);
fullWidth = input<boolean>(false);
private alignMap: Record<AlignOption, string> = {
start: 'flex-start',
center: 'center',
end: 'flex-end',
stretch: 'stretch',
baseline: 'baseline',
};
private justifyMap: Record<JustifyOption, string> = {
start: 'flex-start',
center: 'center',
end: 'flex-end',
between: 'space-between',
around: 'space-around',
evenly: 'space-evenly',
};
alignValue = computed(() => this.alignMap[this.align()]);
justifyValue = computed(() => this.justifyMap[this.justify()]);
}Services
Theme Service
theme.service.ts:
import { Injectable, signal, computed, effect } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { inject } from '@angular/core';
export type Theme = 'light' | 'dark' | 'system';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private document = inject(DOCUMENT);
/** Current theme setting */
theme = signal<Theme>('system');
/** Resolved theme (light or dark) */
resolvedTheme = signal<'light' | 'dark'>('light');
constructor() {
// Load stored preference
const stored = localStorage.getItem('theme') as Theme | null;
if (stored) this.theme.set(stored);
// Watch for system changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => this.updateResolved());
// React to theme changes
effect(() => {
this.updateResolved();
});
}
setTheme(theme: Theme): void {
this.theme.set(theme);
localStorage.setItem('theme', theme);
}
private updateResolved(): void {
let resolved: 'light' | 'dark';
if (this.theme() === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
resolved = prefersDark ? 'dark' : 'light';
} else {
resolved = this.theme() as 'light' | 'dark';
}
this.resolvedTheme.set(resolved);
this.document.documentElement.dataset['theme'] = resolved;
}
}Usage:
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-theme-toggle',
template: `
<button (click)="toggleTheme()">
Current: {{ themeService.resolvedTheme() }}
</button>
`,
})
export class ThemeToggleComponent {
themeService = inject(ThemeService);
toggleTheme(): void {
const next = this.themeService.resolvedTheme() === 'light' ? 'dark' : 'light';
this.themeService.setTheme(next);
}
}Directives
Focus Trap Directive
focus-trap.directive.ts:
import {
Directive,
ElementRef,
OnInit,
OnDestroy,
inject,
} from '@angular/core';
@Directive({
selector: '[uiFocusTrap]',
standalone: true,
})
export class FocusTrapDirective implements OnInit, OnDestroy {
private el = inject(ElementRef<HTMLElement>);
private focusableSelector =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
ngOnInit(): void {
this.el.nativeElement.addEventListener('keydown', this.handleKeydown);
this.focusFirst();
}
ngOnDestroy(): void {
this.el.nativeElement.removeEventListener('keydown', this.handleKeydown);
}
private handleKeydown = (event: KeyboardEvent): void => {
if (event.key !== 'Tab') return;
const focusable = this.el.nativeElement.querySelectorAll<HTMLElement>(
this.focusableSelector
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};
private focusFirst(): void {
const first = this.el.nativeElement.querySelector<HTMLElement>(
this.focusableSelector
);
first?.focus();
}
}Component Communication
Input transforms
import { booleanAttribute, numberAttribute } from '@angular/core';
@Component({...})
export class ExampleComponent {
// Transform string "true"/"false" to boolean
disabled = input(false, { transform: booleanAttribute });
// Transform string to number
count = input(0, { transform: numberAttribute });
// Custom transform
items = input<string[]>([], {
transform: (value: string | string[]) =>
typeof value === 'string' ? value.split(',') : value,
});
}Content projection with selectors
@Component({
selector: 'ui-card',
template: `
<div class="card">
<header class="card__header">
<ng-content select="[cardHeader]" />
</header>
<div class="card__body">
<ng-content />
</div>
<footer class="card__footer">
<ng-content select="[cardFooter]" />
</footer>
</div>
`,
})
export class CardComponent {}
// Usage:
// <ui-card>
// <h2 cardHeader>Title</h2>
// <p>Body content</p>
// <button cardFooter>Action</button>
// </ui-card>Library Export
public-api.ts:
// Components
export * from './lib/components/primitives/button';
export * from './lib/components/primitives/input';
export * from './lib/components/layout/stack';
export * from './lib/components/composite/card';
// Services
export * from './lib/services/theme.service';
// Directives
export * from './lib/directives/focus-trap.directive';
// Types
export type { ButtonVariant, ButtonSize } from './lib/components/primitives/button';
export type { InputSize } from './lib/components/primitives/input';Testing
button.component.spec.ts:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ButtonComponent } from './button.component';
import { By } from '@angular/platform-browser';
describe('ButtonComponent', () => {
let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ButtonComponent],
}).compileComponents();
fixture = TestBed.createComponent(ButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit clicked event', () => {
const spy = jest.spyOn(component.clicked, 'emit');
const button = fixture.debugElement.query(By.css('button'));
button.triggerEventHandler('click', new MouseEvent('click'));
expect(spy).toHaveBeenCalled();
});
it('should be disabled when loading', () => {
fixture.componentRef.setInput('loading', true);
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
expect(button.nativeElement.disabled).toBe(true);
});
it('should apply variant class', () => {
fixture.componentRef.setInput('variant', 'danger');
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
expect(button.nativeElement.classList).toContain('btn--danger');
});
it('should have aria-busy when loading', () => {
fixture.componentRef.setInput('loading', true);
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
expect(button.nativeElement.getAttribute('aria-busy')).toBe('true');
});
});Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

