A demonstration of modern Angular routing patterns that prioritize user experience over traditional blocking guards. This project showcases two UX-friendly alternatives to conventional guards that block navigation until authentication is resolved.
Live demo available at : https://cisstech.github.io/angular-routing-ux/
This demo follows modern Angular practices with a flat file structure and no suffix naming convention:
src/app/
├── components/
│ ├── index.ts # barrel export
│ └── skeleton.ts # reusable skeleton component
├── directives/
│ ├── index.ts # barrel export
│ └── auth-if.ts # structural directive for auth control
├── guards/
│ └── auth-guard.ts # guard implementations
├── pages/
│ ├── demo-auth-if.ts # authIf directive demonstration
│ ├── demo-blocking.ts # traditional blocking guard demo
│ ├── demo-skeleton.ts # skeleton guard demonstration
│ ├── home.ts # application home page
│ └── login.ts # login simulation page
├── services/
│ ├── index.ts # barrel export
│ ├── auth-guard.ts # guard state management service
│ ├── auth.ts # authentication service
│ └── user-api.ts # user data service
├── app.ts # root component
├── app.config.ts # application configuration
└── app.routes.ts # routing configuration
This project demonstrates current Angular best practices:
- No suffix naming: Files are named
auth.ts
,skeleton.ts
instead ofauth.service.ts
,skeleton.component.ts
- Barrel exports: Each directory has an
index.ts
for clean imports - Standalone components: No NgModules, all components are standalone
- Signals: State management uses signals instead of RxJS subjects
- Inline templates and styles: Simplified code with no external CSS/HTML files
- Global CSS sharing: Common styles defined once and reused across components
Angular's traditional route guards (CanActivate
) have significant UX drawbacks:
- Blocking navigation: Users see nothing while authentication resolves
- Poor performance metrics: First Contentful Paint is delayed
- No visual feedback: Applications appear frozen during guard execution
- Lighthouse score impact: Core Web Vitals suffer from delayed rendering
// Traditional blocking guard - blocks until resolved
export const blockingAuthGuard: CanActivateFn = () => {
const authService = inject(Auth);
const router = inject(Router);
return authService.loginStatusChange.pipe(
map(isLoggedIn => {
if (!isLoggedIn) {
router.navigate(['/login']);
return false;
}
return true;
})
);
};
The skeleton guard allows immediate page rendering with placeholder content while authentication resolves in the background.
export const skeletonAuthGuard: CanActivateFn = (route) => {
const authService = inject(Auth);
const userService = inject(UserApi);
const router = inject(Router);
const guardService = inject(AuthGuard);
const skeleton = route.data['skeleton'];
const skeletonDelay = route.data['skeletonDelay'] || 300;
// Show skeleton after delay if guard is still resolving
const skeletonTimer = timer(skeletonDelay).pipe(
takeUntil(authService.loginStatusChange),
tap(() => guardService.setSkeleton(skeleton))
);
return authService.loginStatusChange.pipe(
tap(() => skeletonTimer.subscribe()),
mergeMap(isLoggedIn => {
if (!isLoggedIn) {
router.navigate(['/login']);
return of(false);
}
return userService.loadCurrentUser().pipe(
map(() => true),
catchError(() => {
router.navigate(['/login']);
return of(false);
})
);
}),
finalize(() => guardService.setSkeleton(null))
);
};
Guards are configured in route data:
{
path: 'protected',
component: ProtectedPage,
canActivate: [skeletonAuthGuard],
data: {
skeleton: SkeletonOverlay,
skeletonDelay: 300 // milliseconds
}
}
- Dashboard pages: Show layout skeleton while loading user data
- Profile sections: Display structure while fetching user information
- Data-heavy pages: Provide visual feedback during authentication and data loading
- Mobile applications: Improve perceived performance on slower connections
- Layout matching: Skeleton must exactly match final page layout to avoid layout shifts
- Maintenance overhead: Skeleton components need updates when page layout changes
- Content complexity: Not suitable for highly dynamic content structures
- Animation considerations: Skeleton transitions should be smooth to avoid jarring effects
The skeleton system uses shared CSS between host and overlay components:
@Component({
selector: 'app-skeleton',
template: `
<div
class="skeleton"
[class]="'skeleton--' + variant()"
[style.width]="width() || undefined"
[style.height]="height() || undefined"
></div>
`,
styles: [`
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
}
`]
})
export class Skeleton {
variant = input<'line' | 'text' | 'card' | 'button'>('line');
width = input<string>(); // explicit width prevents layout shifts
height = input<string>(); // explicit height maintains space
}
skeletonDelay property: Controls when skeleton appears (default: 300ms). This prevents skeleton flash on fast connections while providing feedback on slower ones.
A structural directive that provides granular authentication control without navigation blocking.
@Directive({
selector: '[authIf]',
standalone: true
})
export class AuthIf {}
<div *authIf="let authState = state; let user = user; redirectOnReject: false; onAccept: onLoadUser.bind(this)">
@switch (authState) {
@case ('pending') {
<app-skeleton variant="card"></app-skeleton>
}
@case ('accepted') {
<h1>Welcome {{ user?.name }}</h1>
}
@case ('denied') {
<p>Please log in to continue</p>
}
}
</div>
class MyComponent {
onLoadUser(user) {
// replace ngOnInit
}
}
- authIfOnAccept: Callback to intercept user resolving
- authIfOnReject: Callback to intercept errors
- authIfRedirectOnReject: Automatically redirects to login when authentication fails (default: true)
- state: Provides current authentication state (
pending
,accepted
,denied
) - user: Exposes current user data when authenticated
This directive can be upgraded to accept roles and check more options on user.
- Mixed content pages: Combine public and protected content on the same page
- Progressive disclosure: Show different content based on user permissions
- Granular control: Apply authentication logic to specific page sections
- Complex layouts: Handle multiple authentication zones independently
Approach | Navigation blocking | First paint | UX impact | Complexity | Use case |
---|---|---|---|---|---|
Traditional guard | Yes | Delayed | Poor | Low | Simple auth check |
Skeleton guard | No | Immediate | Good | Medium | Full page protection |
AuthIf directive | No | Immediate | Excellent | Low | Granular control |
npm install
npm run start
Navigate to http://localhost:4200
to explore the different approaches.
- Performance: Skeleton and AuthIf approaches improve Core Web Vitals scores
- Accessibility: Skeleton animations should respect
prefers-reduced-motion
- SEO: Immediate page rendering improves search engine indexing
- Analytics: Better user engagement metrics due to faster perceived loading
This demo intentionally keeps code simple with inline templates and styles to focus on the core concepts rather than project structure complexity.