Getting Started with Standalone Components in Angular (Complete Modern Guide)

When Angular initially introduced Standalone Components, it marked the beginning of a major architectural shift away from the complex NgModule system. Today, Standalone Components are the official default in Angular (v17, v18, and v19+).
By eliminating NgModule declarations, Standalone Components simplify dependency management, speed up compilation, make lazy loading straightforward, and improve developer onboarding.
⚡ Using Modern Angular (v17+)? Jump directly to Modern Standalone Architecture:
app.config.ts, Signals, and Vitest to see the latest standard project structure.
In this guide, we will explore how to build, import, route, and unit test Standalone Components in modern Angular.
Let's begin by generating our first Standalone Component.
1. Creating a Standalone Component 🧩
In modern Angular CLI (ng new), all components are standalone by default.
If you generate a component via CLI:
ng generate component components/user-cardThe generated component declares its own dependencies inside its @Component metadata:
// user-card.component.ts
import { Component, input, output } from '@angular/core';
export interface User {
id: string;
name: string;
email: string;
}
@Component({
selector: 'app-user-card',
standalone: true, // 👈 Default in modern Angular
imports: [], // 👈 Directly declare other components, directives, or pipes
template: `
<div class="user-card p-4 border rounded-xl shadow-xs">
<h3 class="font-bold">{{ user().name }}</h3>
<p class="text-neutral-500">{{ user().email }}</p>
<button (click)="remove.emit(user().id)" class="btn-danger mt-2">
Remove
</button>
</div>
`
})
export class UserCardComponent {
readonly user = input.required<User>();
readonly remove = output<string>();
}Now let's see how to use one Standalone Component inside another.
2. Importing Standalone Components into Other Components 🔗
When a parent component needs to render a child standalone component, you add the child directly to the parent's imports: [...] array:
// user-list.component.ts
import { Component, signal } from '@angular/core';
import { UserCardComponent, User } from '../user-card/user-card.component';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent], // 👈 Direct dependency declaration
template: `
<section class="user-list space-y-4">
<h2>Active Team Members</h2>
@for (member of members(); track member.id) {
<app-user-card [user]="member" (remove)="onRemoveMember($event)" />
} @empty {
<p class="text-neutral-500">No members found.</p>
}
</section>
`
})
export class UserListComponent {
readonly members = signal<User[]>([
{ id: '1', name: 'Dany Paredes', email: 'dany@example.com' },
{ id: '2', name: 'Alex Johnson', email: 'alex@example.com' }
]);
onRemoveMember(id: string) {
this.members.update(list => list.filter(u => u.id !== id));
}
}Notice that you do not need CommonModule just for loops or conditionals—modern Angular @for and @if built-in control flow works natively everywhere!
Now let's look at how routing and lazy loading work with Standalone Components.
3. Route Configuration & Lazy Loading with loadComponent 🚀
In module-based Angular, lazy loading required a separate NgModule file with loadChildren. With Standalone Components, you lazy load individual components directly using loadComponent:
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: 'users',
pathMatch: 'full'
},
{
path: 'users',
// Lazy load the standalone component on demand!
loadComponent: () => import('./components/user-list/user-list.component')
.then(m => m.UserListComponent)
},
{
path: 'settings',
loadComponent: () => import('./pages/settings/settings.component')
.then(m => m.SettingsComponent)
}
];The Angular bundler will automatically create a dedicated JavaScript chunk for each route.
Now let's see how the entire application bootstraps without an AppModule.
4. Modern Standalone Architecture: app.config.ts, Signals, and Vitest ⚡
In modern Angular, AppModule is completely gone. Applications bootstrap using bootstrapApplication in main.ts, configured via app.config.ts:
app.config.ts: Application Providers
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withFetch())
]
};main.ts: Clean Bootstrap
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));Now let's see how easy it is to write unit tests for Standalone Components using Vitest.
5. Testing Standalone Components with Vitest 🧪
Because Standalone Components declare their own dependencies, writing unit tests in Vitest is fast and requires zero module setup:
// user-card.component.spec.ts (Vitest)
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent, User } from './user-card.component';
import { ComponentRef } from '@angular/core';
describe('UserCardComponent with Vitest', () => {
let component: UserCardComponent;
let componentRef: ComponentRef<UserCardComponent>;
let fixture: ComponentFixture<UserCardComponent>;
const mockUser: User = { id: '101', name: 'Dany Paredes', email: 'dany@example.com' };
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent] // 👈 Import standalone component directly!
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
componentRef = fixture.componentRef;
// Set signal input value
componentRef.setInput('user', mockUser);
fixture.detectChanges();
});
it('should render user information correctly', () => {
const el = fixture.nativeElement as HTMLElement;
expect(el.querySelector('h3')?.textContent).toBe('Dany Paredes');
expect(el.querySelector('p')?.textContent).toBe('dany@example.com');
});
it('should emit remove event with user ID when clicked', () => {
let emittedId = '';
component.remove.subscribe((id) => (emittedId = id));
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(emittedId).toBe('101');
});
});Legacy NgModule vs Modern Standalone Architecture 📊
| Aspect | Legacy Angular (v2 – v16) | Modern Angular (v17+) |
|---|---|---|
| Component Registration | Must be declared in an @NgModule | standalone: true (Default) |
| Dependency Imports | Injected via NgModule.imports | Declared directly in @Component.imports |
| Application Bootstrap | platformBrowserDynamic().bootstrapModule(AppModule) | bootstrapApplication(AppComponent, appConfig) |
| Lazy Loading | loadChildren: () => import(...).then(m => m.MyModule) | loadComponent: () => import(...).then(m => m.MyComponent) |
| Unit Testing | Heavy TestBed with module fixtures | Lightweight TestBed or Vitest directly |
Recap 🛠️
Standalone Components make Angular cleaner and faster:
- Components, directives, and pipes declare their own dependencies directly via
imports: [...]. - Use
bootstrapApplicationandapp.config.tsto configure providers cleanly. - Use
loadComponentfor instant route-level lazy loading without intermediary routing modules. - Test Standalone Components effortlessly using Vitest.
For more modern Angular guides, check out my articles on How to Use ng-template, ng-container, and ng-content and How to Use Route Parameters with Signals!
These are my experiences learning and facing my daily challenges working with Angular.
Frequently Asked Questions
What are standalone components in Angular and how are they different from module-based components?
Standalone components do not belong to any NgModule. Instead, they declare their own dependencies directly in the component imports array. In modern Angular (v17+), standalone is the default for all newly generated components, directives, and pipes.
How do you bootstrap an Angular application without an AppModule?
In main.ts, call 'bootstrapApplication(AppComponent, appConfig)' where appConfig contains your application-level providers configured with provideRouter(routes), provideHttpClient(), and other functional providers.
How does lazy loading work with standalone components?
Instead of loadChildren with an NgModule, use 'loadComponent: () => import('./path/to/component').then(m => m.MyComponent)' directly in your route configuration.
How do you test standalone components with modern Vitest in Angular?
In your Vitest test file, import the standalone component directly into TestBed.configureTestingModule({ imports: [MyStandaloneComponent] }) or use ng-mocks MockBuilder, without needing an NgModule harness.
Related Articles
How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)
Learn the exact differences between ng-template, ng-container, and ng-content in Angular, how to pass context with ngTemplateOutlet, multi-slot projection, and how they evolve with modern @if, @for, and @defer control flow.
How to Use Route and Query Parameters in Angular (with Signals & Inputs)
Learn how to pass and read route parameters, query parameters, and matrix parameters in Angular using Signals, withComponentInputBinding, and ActivatedRoute.
How To Test Components In Angular Using TestBed
Learn how to unit test Angular components with TestBed, handle child components and service mocks, and switch from Karma/Jasmine to modern Vitest with Standalone and Signals.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.