Angular Setup Guide
Airframe is CSS-only. No Angular-specific package needed. Install the core CSS, import it, and use Airframe classes directly in your templates.
Works with Angular 17+ standalone components and NgModule applications.
Quick Start
Section titled “Quick Start”1. Install
Section titled “1. Install”npm install @airframeui/core2. Import CSS
Section titled “2. Import CSS”Using angular.json (recommended)
Add to angular.json under projects.<app>.architect.build.options:
"styles": [
"node_modules/@airframeui/core/core.css",
"src/styles.scss"
]Using styles.scss
Alternatively, import directly in src/styles.scss:
@import "@airframeui/core/core.css";3. Use in templates
Section titled “3. Use in templates”@Component({
selector: 'app-root',
standalone: true,
template: `
<div class="af-container">
<div class="af-stack af-gap-lg">
<h1>Welcome</h1>
<button class="af-btn">Get Started</button>
</div>
</div>
`
})
export class AppComponent {}That’s it. No wrappers required. No modules to import. Already have a kit? Wrap af-* in <ui-button>. Use as much as you need.
ESLint
Section titled “ESLint”Airframe rules run on Angular 20+ templates when ESLint uses @angular-eslint/template-parser. Spread airframe.configs['flat/angular'] next to angular-eslint template config. processInlineTemplates covers inline template strings.
// eslint.config.js
import airframe from '@airframeui/eslint-plugin';
import angular from 'angular-eslint';
export default [
...airframe.configs['flat/recommended'],
{
files: ['**/*.ts'],
processor: angular.processInlineTemplates,
},
...angular.configs.templateRecommended,
...airframe.configs['flat/angular'],
];Install @airframeui/eslint-plugin as a dev dependency. In this repo, examples/angular is the working setup (pnpm example:angular). Full rule list: ESLint plugin.
Leave <body> without af-body in Angular apps. CDK overlays and other portals append to body; af-body is only for pinning a direct-child <footer> to the viewport. Body.
Skip, overlay, and routed layouts
Section titled “Skip, overlay, and routed layouts”Put af-skip first in the app root, as a sibling of sticky af-navbar — not inside a transformed layout. tabindex="-1" on <main>. One #main per routed view; never nest <main>.
Overlay nav: set --af-navbar-height to the real bar (default 3.5rem). Close <details class="af-nav-collapse"> on navigate. CSS locks scroll while overlay is open; keep focus trap in the app.
Pattern URLs match catalog ids: /docs/patterns/skip, /docs/patterns/nav-collapse.
Standalone Component Examples
Section titled “Standalone Component Examples”Dashboard Layout
Section titled “Dashboard Layout”@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<div class="af-container">
<div class="af-stack af-gap-lg">
<header class="af-inline af-justify-between">
<h1>Dashboard</h1>
<button class="af-btn">New Item</button>
</header>
<div class="af-grid af-grid-3@lg af-gap-lg">
<div class="af-card">
<h3 class="af-card__title">Revenue</h3>
<p class="af-text-h2">$84,200</p>
</div>
<div class="af-card">
<h3 class="af-card__title">Users</h3>
<p class="af-text-h2">12,340</p>
</div>
<div class="af-card">
<h3 class="af-card__title">Growth</h3>
<p class="af-text-h2">+18%</p>
</div>
</div>
<div class="af-grid">
<div class="af-col-span-8@lg">
<div class="af-card">
<h2 class="af-card__title">Main Content</h2>
<p class="af-card__body">Content goes here</p>
</div>
</div>
<div class="af-col-span-4@lg">
<div class="af-card">
<h2 class="af-card__title">Sidebar</h2>
<p class="af-card__body">Sidebar content</p>
</div>
</div>
</div>
</div>
</div>
`
})
export class DashboardComponent {}Form with Reactive Forms
Section titled “Form with Reactive Forms”import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-contact',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<div class="af-container-sm">
<form class="af-stack af-gap-lg" [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="af-field">
<label for="name" class="af-field__label">Name</label>
<input type="text" id="name" class="af-input" formControlName="name" />
</div>
<div class="af-field">
<label for="email" class="af-field__label">Email</label>
<input type="email" id="email" class="af-input" formControlName="email" />
<p class="af-field__help">We'll never share your email.</p>
</div>
<div class="af-field">
<label for="message" class="af-field__label">Message</label>
<textarea id="message" class="af-textarea" rows="5" formControlName="message"></textarea>
</div>
<div class="af-inline af-gap-md">
<button type="submit" class="af-btn" [disabled]="form.invalid">Send</button>
<button type="button" class="af-btn af-is-secondary" (click)="form.reset()">Clear</button>
</div>
</form>
</div>
`
})
export class ContactComponent {
form = inject(FormBuilder).group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
message: ['', Validators.required],
});
onSubmit(): void {
console.log(this.form.value);
}
}Navigation with Router
Section titled “Navigation with Router”import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-layout',
standalone: true,
imports: [RouterLink, RouterLinkActive, RouterOutlet],
template: `
<nav class="af-navbar">
<div class="af-container">
<a routerLink="/" class="af-navbar-brand">My App</a>
<ul class="af-navbar-nav">
<li>
<a routerLink="/dashboard" routerLinkActive="af-is-active">Dashboard</a>
</li>
<li>
<a routerLink="/settings" routerLinkActive="af-is-active">Settings</a>
</li>
</ul>
</div>
</nav>
<main class="af-container af-section-lg">
<router-outlet />
</main>
`
})
export class LayoutComponent {}Data Table
Section titled “Data Table”import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-table',
standalone: true,
template: `
<div class="af-card">
<table class="af-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@for (user of users(); track user.id) {
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span class="af-badge">{{ user.role }}</span>
</td>
<td>
<div class="af-inline af-gap-sm">
<button class="af-btn af-is-sm">Edit</button>
<button class="af-btn af-is-sm af-is-secondary">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
`
})
export class UserTableComponent {
users = input<{ id: number; name: string; email: string; role: string }[]>([]);
}Theming
Section titled “Theming”Override --af-* in styles.scss. Force light or dark with data-theme on <html>. Full workflow: Theming.
@Component({
selector: 'app-theme-toggle',
standalone: true,
template: `
<button class="af-btn af-is-sm" (click)="toggleTheme()">Toggle {{ isDark ? 'Light' : 'Dark' }}</button>
`
})
export class ThemeToggleComponent {
isDark = false;
toggleTheme(): void {
this.isDark = !this.isDark;
document.documentElement.setAttribute('data-theme', this.isDark ? 'dark' : 'light');
}
}View Encapsulation
Section titled “View Encapsulation”Airframe classes work with Angular’s default view encapsulation. If you need to style Airframe elements from a parent component, use :host or set encapsulation to None:
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-custom',
standalone: true,
encapsulation: ViewEncapsulation.None,
styles: `
.my-custom-card {
--af-card-padding: var(--af-space-6);
--af-card-radius: var(--af-radius-xl);
}
`,
template: `
<div class="af-card my-custom-card">
<h2 class="af-card__title">Custom Card</h2>
</div>
`
})
export class CustomComponent {}SSR (Angular Universal)
Section titled “SSR (Angular Universal)”Airframe CSS works with Angular SSR out of the box. No special configuration needed. The CSS is loaded via angular.json styles array, which works in both client and server builds.
Troubleshooting
Section titled “Troubleshooting”CSS not loading
Section titled “CSS not loading”- Check that
@airframeui/core/core.cssis in thestylesarray inangular.json - Or check that
src/styles.scssimports it and is itself in thestylesarray
Styles not applying
Section titled “Styles not applying”- Use
classnotclassName(Angular usesclass) - Check that no conflicting CSS is overriding Airframe styles
- If view encapsulation is blocking styles, override tokens via CSS variables instead of targeting Airframe class internals
Build errors
Section titled “Build errors”- Angular 17+ required
- Node 20+ required by the Angular CLI toolchain (Airframe CSS itself has no Node runtime requirement)
- Run
npm installto ensure@airframeui/coreis installed
Next steps
Section titled “Next steps”- ESLint plugin — leftover Tailwind/Bootstrap and unknown
af-*in templates - Use as much as you need — wrap
af-*if a kit exists - Patterns — buttons, cards, forms, and more
- Theming — CSS variable overrides
- Structure — structural primitives