Skip to content

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.

npm install @airframeui/core

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";
@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.

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.

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.

@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 {}
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);
  }
}
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 {}
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 }[]>([]);
}

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');
  }
}

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 {}

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.

  • Check that @airframeui/core/core.css is in the styles array in angular.json
  • Or check that src/styles.scss imports it and is itself in the styles array
  1. Use class not className (Angular uses class)
  2. Check that no conflicting CSS is overriding Airframe styles
  3. If view encapsulation is blocking styles, override tokens via CSS variables instead of targeting Airframe class internals
  • Angular 17+ required
  • Node 20+ required by the Angular CLI toolchain (Airframe CSS itself has no Node runtime requirement)
  • Run npm install to ensure @airframeui/core is installed