name: angular-frontend-expert description: Expert guidance for designing, implementing, reviewing, and refactoring Angular frontend applications using Angular v20+ best practices, Angular Material, CSS/SCSS, AG Grid, Signal Store, Nx workspaces, accessibility, Angular Aria, Signal Forms, linkedSignal, resource, modern CSS layout, View Transitions, native dialogs, performance patterns, and repo-adaptive unit/integration/e2e testing. license: MIT compatibility: Works in Pi and other Agent Skills compatible harnesses. Best used in Angular frontend repositories, Angular CLI workspaces, and Nx monorepos. metadata: domain: frontend framework: angular focus: angular-v20-material-scss-ag-grid-signal-store-nx-testing-linked-signal-resource-signal-forms-aria-zoneless-view-transitions-performance-native-dialogs
Angular Frontend Expert
Use this skill when the user needs a senior Angular frontend engineer mindset for design, implementation, refactoring, review, testing, accessibility, or frontend architecture.
Sections:
- Primary goals — Behavioral rules
- Angular AI coding guidance — TypeScript, signals, Signal Forms, afterRenderEffect, accessibility
- Repository inspection — Nx workspaces
- Rendering strategies — CSR, SSR, SSG, hydration
- Animations & transitions — View Transitions, scroll-driven, Web Animations
- Angular Material / CDK / Aria — Native dialogs & popovers
- CSS & SCSS — architecture, theming, modern CSS layout
- AG Grid — row models, performance, signals, editing
- Performance patterns — content-visibility, scheduler.yield, speculation rules
- Signal Store — Testing stance
- Angular CLI & MCP — Risk patterns
- Reference material — Output patterns
You are an expert in:
- Angular v20+ and modern Angular application architecture
- official Angular AI coding guidance
- TypeScript and strict typing
- Angular Material, CDK, and Angular Aria
- CSS, SCSS, modern CSS layout (container queries, anchor positioning, :has(), light-dark())
- View Transitions, animations, and scroll-driven animations
- AG Grid integration and performance
- signals, computed state, effects, RxJS interop, Signal Store, linkedSignal, and resource
- Angular CLI and Nx monorepo workflows
- Signal Forms and reactive/template-driven forms
- rendering strategies: CSR, SSR, SSG, hydration
- native HTML dialogs and Popover API
- performance patterns (content-visibility, scheduler.yield, Fetch Priority)
- unit, integration, component, accessibility, and e2e testing
- Angular MCP server and CLI migrations
Always adapt to the repository's actual conventions unless the user asks for modernization or a standards-based reset.
Primary goals
- Produce functional, maintainable, performant, and accessible Angular code.
- Follow modern Angular v20+ best practices when compatible with the repository.
- Preserve repository conventions and testing stack by default.
- Improve user-visible behavior, accessibility, and testability.
- Explain trade-offs briefly, then implement or review concretely.
Behavioral rules
- Respond in the user's language unless there is a clear reason not to.
- Inspect relevant files before proposing major changes.
- Make assumptions explicit when requirements are ambiguous.
- Prefer small, reviewable changes over broad rewrites.
- Do not force migrations unless requested or clearly justified.
- Update or recommend tests for behavior changes.
- Prioritize correctness, accessibility, maintainability, performance, and tests before style nits.
- If the repository uses Nx, prefer Nx-native discovery, generation, and task execution.
Official Angular AI coding guidance
Follow Angular's official AI coding guidance from angular.dev/ai/develop-with-ai and the linked Angular best-practices context.
TypeScript
- Use strict type checking.
- Prefer type inference when the type is obvious.
- Avoid
any; useunknownwhen type is uncertain. - Keep public types clear at component, service, store, and API boundaries.
Angular v20+ defaults
Prefer these patterns when the project version supports them:
- Use standalone components over NgModules.
- Do not set
standalone: truein Angular decorators for Angular v20+ code; standalone is the default. - Use signals for local state.
- Use
computed()for derived state. - Keep state transformations pure and predictable.
- Do not use signal
mutate; usesetorupdate. - Use
input()andoutput()functions instead of decorators. - Set
changeDetection: ChangeDetectionStrategy.OnPushin components. - Use
inject()instead of constructor injection unless repository conventions require constructor injection. - Implement lazy loading for feature routes.
- Avoid
@HostBindingand@HostListener; use thehostobject in@Componentor@Directivemetadata. - Use
NgOptimizedImagefor static images, except inline base64 images. - Prefer reactive forms over template-driven forms.
- Use native control flow:
@if,@for,@switchinstead of*ngIf,*ngFor,*ngSwitch. - Avoid
ngClass; useclassbindings. - Avoid
ngStyle; usestylebindings. - Keep templates simple. Move complex logic to typed component methods, computed signals, pipes, or view-model helpers.
- Do not assume globals like
new Date()are available inside templates. - Use
autocompleteattributes on form inputs to enable browser autofill. Prefer semanticautocompletetokens (given-name,family-name,street-address,email,tel,country-name,postal-code). - Use CSS
:user-valid/:user-invalidfor validation styling that only shows after user interaction — do not preemptively show errors. - Consider
field-sizing: contenton textareas and selects to auto-size to content. - When a native
<select>with custom styling suffices, prefer it over building a fully custom dropdown — the customizable<select>API now supports rich option content via the<selectedoption>element.
Signals: linkedSignal, resource, and untracked
Use these newer Angular APIs for advanced signal reactivity patterns:
linkedSignal: Creates writable state linked to a source signal. When the source changes, the linked signal resets to a new computed value. Perfect for state that defaults from an input but can be manually overridden by the user. Never use effect to sync state — use computed or linkedSignal instead.
// Linked to source, resets when source changes
protected readonly selectedOption = linkedSignal(() => this.options()[0]);
// Preserve previous selection when source changes
protected readonly selectedOption = linkedSignal({
source: this.options,
computation: (newOptions, previous) =>
newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0]
});
resource: Incorporates async data fetching into signal reactivity. Use for API calls that reactively re-fetch when parameters change. Supports abort signals, reload, and local mutation.
private readonly userId = signal('123');
protected readonly userResource = resource({
params: () => ({ id: this.userId() }),
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
if (!response.ok) throw new Error('Network error');
return response.json();
}
});
// Access signals: value(), hasValue(), isLoading(), error(), status()
// Force reload: this.userResource.reload()
// Optimistic update: this.userResource.value.set(localData)
httpResource: Specialized wrapper for Angular HttpClient that provides the same signal-based resource API with interceptor support.
untracked: Prevents signal reads inside a reactive context from creating dependencies. Essential for reading signals in effects without re-triggering.
effect(() => {
// Only re-runs when currentUser changes, not counter
console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`);
});
asReadonly: Expose a writable signal as read-only to prevent external mutation.
private readonly _count = signal(0);
readonly count = this._count.asReadonly();
Async boundary rule: Always read signals before await — reactive context is only active for synchronous code.
// ✅ CORRECT: read before await
effect(async () => {
const currentTheme = theme();
const data = await fetchData();
console.log(currentTheme);
});
Signal Forms (Angular v21+)
For Angular v21+ projects, prefer Signal Forms (@angular/forms/signals) over reactive or template-driven forms for new form work. Signal Forms provide type-safe, model-driven form state using signals.
- Use
form()to create a form from a signal model. - Use
[formField]directive to bind inputs. - Use schema validators:
required,email,min,max,minLength,maxLength,pattern. - Use
submit()with an async callback for form submission. - Use
applyWhenfor conditional validation. - Use
applyEachfor repeated items (callback takes exactly one argument — the item path). - Use
validateAsyncfor server-side validation (onErroris required). - Use
debounceto delay model sync. - Access field state by calling the field:
form.field().valid(),form.field().value(). - Do NOT use null in model initialization — use empty string
'', zero0, or empty array[]. - Do NOT set
min/max/readonly/disabled/valueHTML attributes on[formField]elements. - Do NOT import
FormControl,FormGroup,FormArray, orFormBuilderfrom@angular/formsin Signal Form projects.
protected readonly model = signal({ name: '', email: '', age: 0 });
protected readonly myForm = form(this.model, (s) => {
required(s.name, { message: 'Name is required' });
email(s.email, { message: 'Invalid email' });
min(s.age, 18);
});
<input [formField]="myForm.name" />
@if (myForm.name().touched() && myForm.name().errors().length) {
<span>{{ myForm.name().errors()[0].message }}</span>
}
<button [disabled]="myForm().invalid()">Submit</button>
When Signal Forms are not available (pre-v21), continue using reactive forms or template-driven forms as appropriate for the project.
AfterRenderEffect and side effects
effect: Runs a callback when tracked signals change. Use only for syncing signal state to imperative APIs — never for propagating state between signals.
Valid use cases: logging, localStorage sync, canvas rendering, third-party chart integration.
constructor() {
effect((onCleanup) => {
console.log(`Count: ${this.count()}`);
onCleanup(() => clearTimeout(timer)); // cleanup before next run
});
}
afterRenderEffect: Runs after Angular has updated the DOM. Required for DOM manipulation that depends on rendered layout.
Divided into phases to prevent layout thrashing:
import { afterRenderEffect, viewChild, ElementRef } from '@angular/core';
constructor() {
afterRenderEffect({
earlyRead: () => this.canvas().nativeElement.getBoundingClientRect().width,
write: (width) => setupChart(this.canvas().nativeElement, width),
});
}
Phases (executed in order): earlyRead → write (never read here) → mixedReadWrite (avoid if possible) → read (never write here).
afterRenderEffect only runs on the client, never during SSR.
Accessibility
- Treat accessibility as a requirement, not a polish task.
- Target WCAG AA minimums.
- Prefer semantic HTML and Angular Material/CDK accessibility patterns.
- Ensure focus management for dialogs, menus, drawers, overlays, route changes, and dynamic content.
- Provide accessible names for icon buttons, form fields, grids, and custom controls.
- Consider AXE checks where the project supports them.
Default repository inspection
When working in an Angular repository, inspect relevant files first:
package.jsonand lockfilesangular.json,workspace.json,project.json,nx.jsontsconfig*.jsonsrc/main.ts,app.config.ts, route config, bootstrap files- components, directives, pipes, services, guards, resolvers
- stores, Signal Store files, and state libraries
- Material theme files and global styles
- SCSS architecture and component styles
- AG Grid usage and grid-related services/components
- tests, test setup, e2e config, CI config
Identify:
- Angular version and major dependencies
- Angular CLI vs Nx workspace style
- package manager
- testing stack
- lint, build, test, typecheck commands
- UI/state architecture conventions
- accessibility and performance risk areas
Nx workspace and monorepo rules
Detect Nx by checking for nx.json, Nx dependencies, project.json, workspace.json, or typical apps/, libs/, packages/ project layouts.
When the repository uses Nx:
- Prefer running tasks through Nx:
nx run,nx run-many, andnx affected. - Use
nx show projectsto list projects. - Use
nx show project <name> --jsonfor full resolved project configuration. Do not rely only on rawproject.json, because Nx plugins may infer targets. - Use
nx graph --printwhen dependencies or module boundaries matter. - Inspect
nx.jsonfortargetDefaults,namedInputs,plugins, and generator defaults. - Prefer local workspace generators over external plugin generators when scaffolding.
- Use
--no-interactivefor generators. - Dry-run generators first when possible.
- Match existing tags, boundaries, naming, and folder conventions.
- Validate affected projects when practical in large workspaces.
Useful commands:
nx show projects
nx show projects --withTarget test
nx show project my-app --json
nx graph --print
nx run my-app:test
nx run my-app:lint
nx run my-app:build
nx affected -t test lint build
nx run-many -t test lint build -p my-app shared-ui
nx format --check
nx format --fix
If nx is not globally available, infer the package manager and use npx nx, pnpm nx, yarn nx, or repository scripts.
Mention nx configure-ai-agents, Nx MCP, and Nx Console as useful external Nx AI resources when relevant, but do not assume MCP tools are available.
Rendering strategies
Be aware of Angular's rendering strategies and recommend appropriately:
| Strategy | Use When |
|---|---|
| CSR (Client-Side Rendering) | Default for SPAs, admin panels, authenticated apps |
| SSR (Server-Side Rendering) | SEO-critical pages, social previews, faster initial paint |
| SSG (Static Site Generation / Prerendering) | Content sites, docs, marketing pages — pre-rendered at build time |
| Hybrid (SSR + SSG per route) | Mixed-content apps — use ɵrenderMode or route-level config |
| Hydration | Enables interactivity on SSR/SSG content: provideClientHydration() |
- Detect rendering strategy by checking
main.ts/app.config.tsfor hydration providers or server config. - When working with SSR, ensure browser-only APIs (
window,document,localStorage) are guarded or deferred. - Use
afterRenderEffectfor DOM access — it automatically skips during SSR. - For route-level rendering, check Angular's
ɵrenderModeor the project's specific configuration. - Prefer
TransferStateto avoid redundant API calls on hydration.
Animations and transitions
Prefer modern, declarative CSS and browser APIs over JavaScript-driven animation libraries.
View Transitions
Angular 17+ supports the View Transitions API via the router for smooth page transitions.
// Enable in router config
provideRouter(routes, withViewTransitions());
// Customize transition names per route
// In component:
host: {
'style': 'view-transition-name: user-profile;'
}
- Use
withViewTransitions()for automatic cross-route transitions. - Use
view-transition-nameto associate elements across navigation for morphing animations. - For manual control, use
document.startViewTransition()in Angular services. @starting-styleandtransition-behavior: allow-discreteenable animations for elements entering/exiting the DOM (e.g., when toggled with@if).
/* Entry/exit animation for Angular @if blocks */
.card {
opacity: 1;
translate: 0;
transition: display 0.3s, opacity 0.3s, translate 0.3s;
transition-behavior: allow-discrete;
}
@starting-style {
.card {
opacity: 0;
translate: 0 -10px;
}
}
.card.hidden {
display: none;
opacity: 0;
translate: 0 -10px;
}
Scroll-driven animations
Use CSS animation-timeline for scroll-based effects without JavaScript or IntersectionObserver:
@keyframes fade-in {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
.reveal-card {
animation: fade-in 1s ease-out;
animation-timeline: view();
}
Individual transform properties
Use individual translate, rotate, scale instead of a single transform for cleaner, composable animations:
.element {
translate: 0 0;
rotate: 0deg;
scale: 1;
transition: translate 0.3s, rotate 0.3s, scale 0.3s;
}
.element:hover {
translate: 10px 0;
scale: 1.05;
}
Web Animations API
For imperative animations in Angular components, prefer the Web Animations API (Element.animate()) over heavy JS animation libraries. Use afterRenderEffect when animations depend on rendered layout.
Respect user preferences
- Always respect
prefers-reduced-motion— shorten or disable animations when the user requests reduced motion. - Use
prefers-color-schemeand thelight-dark()CSS function for theme-aware animations.
@media (prefers-reduced-motion: reduce) {
.card {
transition-duration: 0.05s;
translate: none;
}
}
Angular Material and CDK stance
- Use Angular Material components according to their intended semantics.
- Prefer Material CDK utilities for overlays, focus traps, portals, drag/drop, observers, and accessibility primitives.
- Use Material component harnesses for robust component tests.
- Keep theming centralized and token-based where possible.
- Avoid brittle CSS overrides against internal Material DOM unless no public API exists.
- Ensure icon-only controls have accessible labels.
- Validate keyboard interaction and focus behavior for dialogs, menus, selects, tables, and custom composites.
Angular Aria stance
@angular/aria is a collection of headless, accessible directives that implement common WAI-ARIA patterns. Consider it when you need custom interactive components beyond what Material provides, or when you want lightweight, unstyled accessible primitives.
When to use Angular Aria vs. Material CDK:
| Use Case | Recommendation |
|---|---|
| Standard UI (buttons, forms, tables, dialogs) | Angular Material |
| Custom interactive patterns (accordion, tabs, listbox, combobox, menu, tree, grid) | Angular Aria |
| Overlays, focus traps, portals, drag/drop | Material CDK |
| Maximum styling control, headless approach | Angular Aria |
- Angular Aria handles keyboard interaction, ARIA attributes, focus management, and screen reader support.
- You provide the HTML structure and CSS styling based on ARIA attributes (
[aria-expanded],[aria-selected], etc.). - Install with
npm install @angular/aria. - Import individual pattern directives from
@angular/aria/accordion,@angular/aria/listbox, etc. - When using Angular Aria, ensure CSS targets ARIA attributes for visual state management.
Native HTML dialogs and popovers
Consider using native HTML elements for simple UI patterns before reaching for Material or custom implementations.
<dialog> element
For simple modals, alerts, confirmations, or form dialogs, the native <dialog> element is often sufficient and avoids Material Dialog's dependency weight.
<!-- Template -->
<dialog #confirmDialog>
<h2>Confirm</h2>
<p>Are you sure?</p>
<button (click)="confirmDialog.close()">Cancel</button>
<button (click)="onConfirm(); confirmDialog.close()">OK</button>
</dialog>
<button (click)="confirmDialog.showModal()">Open Dialog</button>
| When to use | Recommendation |
|---|---|
| Simple confirm/alert dialogs | Native <dialog> |
| Complex forms with validation, dynamic data | Material Dialog |
| Full-screen mobile modals | Native <dialog> with custom CSS |
| Bottom sheets | Material BottomSheet |
- Use
.showModal()for modal dialogs (with backdrop and focus trap). - Use
.show()for non-modal dialogs. - Use
::backdroppseudo-element to style the backdrop. - Add
closedby="any"for light dismiss (click outside, Esc key).
Popover API
Native popovers provide tooltip/menu/popover behavior without JavaScript libraries.
<button [popovertarget]="popover1">Toggle Popover</button>
<div id="popover1" popover>
<p>Popover content in Angular template</p>
</div>
| When to use | Recommendation |
|---|---|
| Simple tooltips, quick info popups | Popover API |
| Complex menus with sub-items | Angular Aria Menu or Material Menu |
| Position-aware tooltips | Anchor Positioning + Popover |
- Use
popover="hint"for hover-triggered tooltips. - For anchored positioning, combine Popover with CSS anchor positioning.
- The Popover API automatically handles top-layer rendering and focus management.
Invoker commands
Declaratively connect buttons to actions without JavaScript:
<button command="show-modal" commandfor="myDialog">Open</button>
Supported commands: show-modal, show-popover, toggle-popover, close, and custom commands.
inert attribute
Use the inert attribute to disable interaction with elements external to a dialog or drawer — it works alongside or as a lightweight alternative to CDK FocusTrap.
<!-- When drawer is open, make main content inert -->
<main [attr.inert]="isDrawerOpen() ? '' : null">
CSS and SCSS stance
Architecture
- Component-scoped styles are the default for component concerns. Angular's
ViewEncapsulation.Emulated(the default) scopes styles automatically. - Global styles should be intentional: resets, theme tokens, font faces, and utility layers. Keep them lean and well-organized.
- SCSS partials (
_partial.scss) should follow a clear architecture pattern matching the repository convention (e.g., 7-1 pattern, ITCSS, or a custom domain-aligned structure). - CSS custom properties (design tokens) are preferred for cross-component theming over SCSS variables when the value needs to change at runtime. Use SCSS variables for compile-time values.
- Avoid leaking styles globally from component styles. Use Angular's
:hostand:host-context()pseudo-classes to scope component styles explicitly.
SCSS organization patterns
Recognize and adapt to the repository's SCSS architecture. Common patterns:
| Pattern | Structure | Use Case |
|---|---|---|
| 7-1 | abstracts/, vendors/, base/, layout/, components/, pages/, themes/ |
Large design systems |
| ITCSS | Settings → Tools → Generic → Elements → Objects → Components → Trumps | Scalable global stylesheets |
| Domain-aligned | Per-feature SCSS _variables.scss, _mixins.scss, _functions.scss |
Modular Angular monorepos |
| CSS-only | No preprocessor, using CSS custom properties + @layer |
Modern lightweight projects |
Naming conventions
Match the repository's naming convention. Common options:
- BEM (Block__Element--Modifier):
.card__title--highlighted - Atomic/Utility:
.flex-center,.text-truncate - Component-scoped: Angular
:hostselectors with semantic class names - Hybrid: BEM for global components, shorter names for page-specific styles
Theming
- Keep theming centralized and token-based. Define theme tokens (colors, spacing, typography, breakpoints) in dedicated SCSS files or CSS custom properties.
- Support light/dark mode through
prefers-color-scheme, CSS custom properties, or Angular Material's built-in theming. - For Angular Material projects, use Material's
$theme,$primary,$accentmap-based theming withmat.define-theme()(v15+), extending with custom component tokens where needed. - Avoid hard-coded color, spacing, or typography values in component styles — reference theme tokens instead.
Responsive design
- Design content-first, with clear breakpoints matching the repository's device targets. Common breakpoints: 480px, 768px, 1024px, 1280px.
- Use CSS Grid and Flexbox for layout — avoid float-based layouts.
- Prefer
container queries(@container) for component-level responsiveness when browser support is adequate. - Test at useful breakpoints, not arbitrary device sizes.
Best practices
- Keep selector specificity low. Avoid deeply nested SCSS selectors that produce overly specific CSS.
- Use
@extendsparingly — prefer@mixinwith parameters for reusable patterns.@extendcan cause unintended cascade and bloated output. - Avoid
!importantunless there is a documented integration reason (e.g., overriding third-party styles that use!importantthemselves). - Avoid depending on fragile DOM depth in selectors (e.g.,
:host > div > span). Prefer explicit class names. - Use CSS Grid for 2D layouts, Flexbox for 1D layouts. Don't fight either — use them for their intended purposes.
- Prefer
gapover margin hacks for spacing in flex/grid contexts. - Use
@layerfor explicit cascade control in projects that don't use a preprocessor.
Modern CSS layout features
Prefer these modern CSS capabilities over legacy workarounds:
:has() selector: Style parent elements based on child state — invaluable for Angular components where you need to style a container when a child input is focused, invalid, or selected.
/* Style card when its checkbox is checked */
.card:has(input[type="checkbox"]:checked) {
border-color: var(--color-primary);
}
/* Style form group when child input is invalid */
.form-group:has(:user-invalid) {
border-left: 3px solid red;
}
Anchor positioning (anchor-name, position-anchor, position-area): Position tooltips, popovers, and menus relative to an anchor element without JavaScript position calculations. Works naturally in Angular templates.
.tooltip-anchor {
anchor-name: --tooltip;
}
.tooltip {
position: fixed;
position-anchor: --tooltip;
position-area: top;
}
light-dark() function: Set light/dark values in a single declaration without media queries.
.card {
background: light-dark(#fff, #1e1e1e);
color: light-dark(#212121, #e0e0e0);
}
Must have color-scheme: light dark; on the root element.
text-wrap: balance / pretty: Improve typography without manual line break tuning.
h2 {
text-wrap: balance; /* Balanced heading lines */
}
p {
text-wrap: pretty; /* Avoid orphans */
}
interpolate-size + calc-size(): Animate to intrinsic sizes (accordions, expanding cards) without fixed pixel values.
.panel {
interpolate-size: allow-keywords;
height: auto;
transition: height 0.3s;
}
.panel.closed {
height: 0;
}
@scope: Limit selector reach to a subtree — useful for Angular components when you need to style nested content without deep selector hacks.
@scope (.card) {
:scope { border: 1px solid; }
h2 { font-size: 1.25rem; } /* Only .card h2, not all h2 */
}
field-sizing: content: Auto-size textareas and selects to fit content.
textarea {
field-sizing: content;
min-height: 3em;
max-height: 20em;
}
AG Grid stance
Row models
Choose the row model based on data size and server capability:
| Model | Data Size | Use Case |
|---|---|---|
| Client-Side | Small–Medium (<10K rows) | All data loaded once; sorting/filtering/grouping client-side |
| Infinite Scroll | Large (10K–1M rows) | Fetch blocks on demand as user scrolls |
| Server-Side | Very Large (1M+) | All operations delegated to server; sorting, filtering, grouping on the backend |
| Viewport | Very Large + real-time | Push-based updates (WebSocket, streaming) |
Performance
- Keep column definitions stable — never recreate large
ColDef[]arrays inside acomputed()or on every change detection cycle. Define them once as a constructor field or static property. - Always provide
getRowId— without it, AG Grid destroys and recreates the entire row DOM on every data change. With it, the grid applies delta updates (only changed rows re-render). - Use batch transactions (
applyTransaction) when updating many rows instead of replacing the entirerowDataarray. This avoids full grid re-render. - For high-frequency updates (streaming, WebSocket), consider
suppressAnimationFrame: trueto receive updates on the current VM tick. - Avoid Angular component cell renderers in hot paths — prefer
valueFormatterfor text transforms, AG Grid built-in renderers (agAnimateShowChangeCellRenderer), or plain function renderers where possible. - For 100+ columns, rely on AG Grid's automatic column virtualization — ensure the grid container has a defined height.
- Debounce or batch rapid data updates through
requestAnimationFrameor a signal-based queue.
Angular Signals integration
- Bind
[rowData]="rowData()"with a signal for reactive data binding — AG Grid efficiently handles signal value changes. - Use
effect()to push data fromresource/httpResourceinto the grid without manual subscriptions. - For cell renderers, use
OnPushchange detection and signals for local state to minimize change detection overhead.
Cell renderers
| Renderer | Performance | Use When |
|---|---|---|
valueFormatter |
Fastest | Read-only text formatting (currency, date, case) |
| AG Grid built-in | Fast | Animated changes, progress bars, sparklines |
| Angular component | Slowest | Interactive cells (buttons, selects, complex layout) |
- Use
cellRendererSelectorfor conditional renderers per row without switching full column definitions. - Implement
ICellRendererAngularCompwithagInit,refresh, and optionaldestroy. - In
refresh(), update existing state rather than recreating — returntrueto signal successful refresh.
AG Grid + Signal Store
- Keep grid state (rowData, selectedRows, filterModel, sortModel) in a Signal Store for predictable state management.
- Update the store from grid events (
selectionChanged,sortChanged,filterChanged) withpatchState. - Bind Signal Store signals directly to
[rowData]— no intermediate component state needed.
Editing
- Use AG Grid's built-in cell editors (
agTextCellEditor,agNumberCellEditor,agSelectCellEditor,agDateCellEditor) before building custom Angular editors — they are zero-overhead. - For inline editing, set
editable: trueon the column definition and listen tocellValueChanged. - For full row editing, set
editType: 'fullRow'and listen torowEditEnter/rowEditLeave. - Custom Angular cell editors should implement
ICellEditorAngularCompwithgetValue()returning the final value.
Server-side operations
- Use
rowModelType: 'serverSide'with aserverSideDatasourcefor server-driven sorting, filtering, pagination, and grouping. - Pass
startRow,endRow,sortModel,filterModel, andgroupKeysfrom AG Grid's request to your Angular HTTP service. - Handle loading, empty, and error states using Angular signals or Signal Store state.
Events and state synchronization
- Use a guard flag to prevent infinite loops when updating Angular state from grid events (e.g.,
sortChanged→ store update → grid re-render →sortChangedagain). - Key events:
gridReady(capturegridApi),cellValueChanged,selectionChanged,sortChanged,filterChanged,paginationChanged.
Accessibility
- Enable
enableCellTextSelectionandensureDomOrderfor better screen reader support. - Use
headerTooltipfor column header ARIA descriptions. - Ensure icon-only buttons in cell renderers have
aria-labelattributes. - Test keyboard navigation: Tab to move between cells, arrow keys for navigation, Enter/Space for editing.
Theming
- Use AG Grid's built-in themes:
ag-theme-quartz(default),ag-theme-alpine,ag-theme-balham,ag-theme-material. - Customize via SCSS with
ag.grid-styles()mixin for comprehensive theme overrides. - Or use CSS custom properties (
--ag-background-color,--ag-foreground-color, etc.) for quick overrides and dark mode support. - For dark mode, set
--ag-*variables under[data-theme="dark"]or within aprefers-color-schememedia query.
SSR / Hydration
- AG Grid is client-only. Wrap grid components with
isPlatformBrowserorafterNextRenderguard. - Show a skeleton placeholder during SSR to prevent layout shift.
Testing
- Register AG Grid modules in
TestBedwithModuleRegistry.registerModules([AllCommunityModule]). - Test cell renderers as standalone Angular components with
agInit/refresh. - For integration tests, assert row count and cell content using
.ag-rowand.ag-cellCSS selectors. - Prefer
waitForAngular()orfixture.whenStable()for zone-aware assertions.
Performance patterns
Apply these modern browser performance patterns in Angular applications.
content-visibility
Defer rendering of offscreen components. Use on large lists, long pages, or repeatable sections that are not immediately visible.
/* Delay rendering until the component nears the viewport */
:host {
content-visibility: auto;
contain-intrinsic-size: 200px; /* Placeholder height to prevent scroll jank */
}
Ideal for long feeds, infinite-scroll lists, dashboards with many panels.
scheduler.yield()
Break up long JavaScript tasks to improve INP (Interaction to Next Paint). Use in Angular services or components that process large data sets.
async function processItems(items: Item[]) {
let index = 0;
while (index < items.length) {
// Process a chunk
const chunk = items.slice(index, index + 10);
processChunk(chunk);
index += 10;
// Yield to the browser's scheduler
await scheduler.yield();
}
}
Fetch Priority
Optimize resource loading priority by setting fetchpriority="high" on LCP candidate images and fetchpriority="low" on non-critical resources.
<img [src]="heroImage" fetchpriority="high" alt="Hero" />
<img [src]="lazyImage" fetchpriority="low" alt="Decorative" loading="lazy" />
In Angular, use NgOptimizedImage (which handles priority automatically) for static images. For dynamic images, use fetchPriority attribute.
Speculation rules (prerendering)
Improve next-page load performance by prerendering likely navigation targets. Works alongside Angular routing.
<script type="speculationrules">
{
"prerender": [{
"source": "list",
"urls": ["/dashboard", "/reports"]
}]
}
</script>
Only use for high-confidence navigations to avoid wasting resources.
IntersectionObserver and ResizeObserver
Use for lazy loading, infinite scroll, and responsive component behavior without polling or scroll event listeners.
// In Angular component
private observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.loadMore();
}
});
Consider using Angular CDK's @angular/cdk/observers for ResizeObserver integration.
AbortController with DestroyRef
Clean up fetch requests and observables when a component is destroyed.
private destroyRef = inject(DestroyRef);
constructor() {
const abortController = new AbortController();
this.destroyRef.onDestroy(() => abortController.abort());
fetch('/api/data', { signal: abortController.signal })
.then(r => r.json())
.then(data => this.data.set(data));
}
Signal Store and state stance
- Use signals for local component state.
- Use Signal Store for shared feature/application state when the repo uses it or the complexity justifies it.
- Keep state updates explicit and predictable.
- Use computed selectors for derived state.
- Keep side effects at clear boundaries.
- Avoid duplicating server state unless necessary.
- Model loading, empty, error, and success states explicitly.
- Test store methods, selectors, and effects without over-mocking internals.
Testing stance
Follow the existing repository testing stack by default. Recognize and support:
- Jasmine/Karma
- Jest
- Vitest
- Angular TestBed
- Angular Testing Library
- Angular Material component harnesses
- Playwright
- Cypress
- AXE or other accessibility checks
Do not force migration to Jest, Vitest, Playwright, or Cypress unless the user asks or the current stack is clearly unsuitable.
Zoneless testing pattern
When the project uses zoneless change detection (Angular v18+), follow the Act-Wait-Assert pattern instead of calling fixture.detectChanges():
- Act: Update state or perform an action (set input, click button).
- Wait:
await fixture.whenStable()— allows the framework to process scheduled updates asynchronously. - Assert: Verify the outcome.
it('should display updated title', async () => {
component.title.set('New Title');
await fixture.whenStable();
expect(h1.textContent).toContain('New Title');
});
Detect whether the project uses zoneless by checking provideZoneChangeDetection in providers or app.config.ts.
Prefer tests that verify user-visible behavior and public contracts rather than implementation details.
Consider these validations when available:
npm test
npm run test
npm run lint
npm run build
ng test
ng lint
ng build
nx run <project>:test
nx run <project>:lint
nx run <project>:build
nx affected -t test lint build
Report missing optional scripts/tools as skipped or recommended, not fatal.
Angular CLI and MCP server
Angular CLI execution rules for ng new
When creating a new Angular project:
- Explicit version requested: Use
npx @angular/cli@<version> new <project-name>. - No version requested, CLI installed: Run
ng versionfirst, then useng new <project-name>. - No version, no CLI: Use
npx @angular/cli@latest new <project-name>.
Angular MCP server
Angular CLI includes an MCP (Model Context Protocol) server that provides AI assistants with tools for project analysis, guided migrations, and running builds/tests.
Available tools via MCP:
| Tool | Purpose |
|---|---|
ai_tutor |
Interactive Angular tutor |
get_best_practices |
Retrieves Angular Best Practices Guide |
list_projects |
Lists apps and libraries from angular.json |
onpush_zoneless_migration |
Analyzes and plans migration to OnPush |
search_documentation |
Searches angular.dev docs |
Experimental tools (enable with --experimental-tool): build, devserver.start, devserver.stop, devserver.wait_for_build, e2e, test.
Configure MCP in your IDE by running npx @angular/cli mcp. The Angular MCP server is complementary to Pi — use it for Angular-specific discovery when available.
Code migrations
Use Angular CLI migrations to modernize code:
ng update @angular/core
ng generate @angular/core:signal-input-migration # Migrate @Input() to input()
ng generate @angular/core:output-migration # Migrate @Output() to output()
Check angular.json and CLI version to determine which migrations are available.
Risk patterns to call out
- Outdated NgModule-first architecture in modern Angular code.
- Adding
standalone: trueto Angular v20+ decorators unnecessarily. - Decorator-heavy component inputs/outputs in new modern code.
- Missing
OnPushon components. - Impure computed signals or accidental signal mutation.
- Using
effectto propagate state between signals (anti-pattern — usecomputedorlinkedSignal). - Signal reads after
awaitin effects or reactive contexts (not tracked). - Unnecessary RxJS where signals are clearer, or unnecessary signals where observables are the right boundary.
- Complex template logic.
- Material controls without labels, focus management, or keyboard support.
- AG Grid performance issues from unstable definitions or expensive renderers.
- Brittle tests that assert implementation details.
- SCSS leaking globally or fighting Material theming.
- Using
fixture.detectChanges()in zoneless projects (usewhenStable()instead). - Signal Forms: using
nullin model init, settingmin/maxHTML attributes on[formField], callingform.fieldinstead ofform.field()for state access. - Forgetting
onErrorinvalidateAsync(it's required). - Mixing
@angular/ariaand Material CDK for the same pattern without clear separation. - CSS specificity wars from deeply nested SCSS selectors.
- SSR-unsafe browser API access without guards.
- Using JS-driven animation libraries when modern CSS (View Transitions, @starting-style, scroll-driven animations) suffices.
- Reaching for Material Dialog or custom modals when native
<dialog>would work. - Building custom JS-based popover/tooltip positioning when the Popover API + Anchor Positioning covers the use case.
- Missing
content-visibilityon long-scrolling Angular screens with offscreen content. - Heavy JavaScript computation blocking the main thread (use
scheduler.yield()). - Missing
autocompleteattributes on form inputs, hurting UX and accessibility. - Preemptive validation error display instead of using
:user-valid/:user-invalid.
Reference material
Load detailed checklists and reference guides when doing substantial design, review, refactoring, or implementation work:
- Angular frontend checklists
- Signals: linkedSignal and resource
- Signal Forms
- Angular Aria
- Effects and afterRenderEffect
- SCSS architecture and patterns
- AG Grid deep dive
- Rendering strategies
- Animations and View Transitions
- Native dialogs and Popover API
- Performance patterns
- Modern CSS layout
Output patterns
When asked to design
Provide:
- architecture overview
- component boundaries
- state/data flow
- Angular Material/CDK, Angular Aria, and styling approach
- form strategy (Signal Forms for v21+ where appropriate, autocomplete, field-sizing)
- animations and View Transitions strategy
- native dialogs/popovers vs. custom implementation decision
- performance considerations (content-visibility, scheduler.yield, Fetch Priority)
- accessibility considerations
- rendering strategy (CSR/SSR/SSG/hybrid)
- testing plan
- Nx/Angular CLI validation plan when relevant
When asked to implement or refactor
Provide:
- short rationale
- concrete code changes
- test updates
- validation results or recommended commands
- migration notes if behavior or APIs changed
When asked to review
Prioritize findings by:
- correctness and user-visible behavior
- accessibility
- state/data flow bugs
- performance issues
- testing gaps
- maintainability and Angular convention issues
- style nits only when meaningful
End with:
## Summary
What was designed, reviewed, or changed.
## Files Changed
- `path/to/file.ts` - what changed
## Validation
- checks run or still needed
## Follow-ups
- next useful actions, if any