Angular v18.x, Angular CLI v18.x (as of 2026-02-10)

Workspace & Library Generation

Begin by creating a new Angular monorepo workspace without an initial application, then generate your UI library.

ng new my-ui-workspace --no-create-application --strict --standalone false --routing false --style scss
// Creates a new Angular workspace without a root application.
// --no-create-application: Skips initial app creation.
// --strict: Enables strict type checking.
// --standalone false: Uses NgModules (still common for libraries, though standalone is default for apps).
// --routing false: No routing module.
// --style scss: Preferred stylesheet format.

cd my-ui-workspace
// Navigate into the new workspace directory.

ng generate library my-ui-components --prefix mui --skip-install
// Generates a new library project named 'my-ui-components'.
// --prefix mui: Sets the prefix for generated components (e.g., <mui-button>).
// --skip-install: Skips running npm install after generation (useful for monorepos).

Component Structure & Public API

Organize your library components and expose them through the public-api.ts file, which defines the library’s public interface.

// projects/my-ui-components/src/lib/button/button.component.ts
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core';

@Component({
  selector: 'mui-button', // Component selector, using library prefix
  template: `
    <button [type]="type" [disabled]="disabled" (click)="onClick.emit($event)">
      <ng-content></ng-content> <!-- Allows projected content inside the button -->
    </button>
  `,
  styleUrls: ['./button.component.scss'],
  standalone: true // Libraries increasingly use standalone components for better tree-shaking
})
export class ButtonComponent {
  @Input() type: 'button' | 'submit' | 'reset' = 'button'; // Input for button type
  @Input() disabled = false; // Input to disable the button
  @Output() onClick = new EventEmitter<Event>(); // Output for click events

  @HostBinding('class.mui-button-disabled') // Adds a class when disabled
  get isDisabledClass(): boolean {
    return this.disabled;
  }
}

// projects/my-ui-components/src/public-api.ts
/*
 * Public API Surface of my-ui-components
 */

export * from './lib/button/button.component'; // Export the standalone component
// For NgModule-based libraries, you would export the module:
// export * from './lib/my-ui-components.module';

Inputs, Outputs & Host Bindings

Components interact via @Input for data flow into the component, @Output for events flowing out, and @HostBinding to dynamically apply host element properties.

// projects/my-ui-components/src/lib/card/card.component.ts
import { Component, Input, HostBinding } from '@angular/core';

@Component({
  selector: 'mui-card',
  template: `
    <div class="mui-card-header" *ngIf="header">
      <h3>{{ header }}</h3>
    </div>
    <div class="mui-card-content">
      <ng-content></ng-content> <!-- Content projection for card body -->
    </div>
  `,
  styleUrls: ['./card.component.scss'],
  standalone: true
})
export class CardComponent {
  @Input() header: string | null = null; // Optional header text
  @Input() elevation: 1 | 2 | 3 | 4 = 1; // Input for card shadow level

  @HostBinding('class.mui-card') readonly hostClass = true; // Always add base class
  @HostBinding('attr.role') readonly hostRole = 'region'; // Accessibility role

  @HostBinding('class.mui-card-elevation-1') get isElevation1() { return this.elevation === 1; }
  @HostBinding('class.mui-card-elevation-2') get isElevation2() { return this.elevation === 2; }
  @HostBinding('class.mui-card-elevation-3') get isElevation3() { return this.elevation === 3; }
  @HostBinding('class.mui-card-elevation-4') get isElevation4() { return this.elevation === 4; }
}

// projects/my-ui-components/src/public-api.ts (updated)
export * from './lib/button/button.component';
export * from './lib/card/card.component'; // Expose the new component

Theming & Accessibility

Implement theming using CSS custom properties for flexibility and ensure basic accessibility attributes are present.

// projects/my-ui-components/src/lib/button/button.component.scss
:host {
  display: inline-block; // Ensure host element behaves correctly
}

button {
  --mui-button-bg: var(--mui-primary-color, #007bff); // Default background, can be overridden
  --mui-button-text-color: var(--mui-text-color-light, #ffffff); // Default text color
  --mui-button-border-radius: 4px;

  padding: 8px 16px;
  border: none;
  border-radius: var(--mui-button-border-radius);
  background-color: var(--mui-button-bg);
  color: var(--mui-button-text-color);
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.2s ease;

  &:hover {
    filter: brightness(0.9);
  }

  &[disabled] {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

// projects/my-ui-components/src/lib/card/card.component.ts (Accessibility update)
import { Component, Input, HostBinding } from '@angular/core';

@Component({
  selector: 'mui-card',
  template: `
    <div class="mui-card-header" *ngIf="header">
      <h3 [attr.id]="headerId">{{ header }}</h3> <!-- Link header to card content -->
    </div>
    <div class="mui-card-content" [attr.aria-labelledby]="header ? headerId : null">
      <ng-content></ng-content>
    </div>
  `,
  styleUrls: ['./card.component.scss'],
  standalone: true
})
export class CardComponent {
  @Input() header: string | null = null;
  @Input() elevation: 1 | 2 | 3 | 4 = 1;

  @HostBinding('class.mui-card') readonly hostClass = true;
  @HostBinding('attr.role') readonly hostRole = 'region';

  readonly headerId = `mui-card-header-${Math.random().toString(36).substring(2, 9)}`; // Unique ID for accessibility
  // ... (elevation HostBindings remain the same)
}

Packaging & Distribution (Monorepo)

Build the library for internal consumption within the same workspace. The Angular CLI automatically configures tsconfig.json paths.

# Build the library for production
ng build my-ui-components --configuration production
# This command compiles the library, outputs to dist/my-ui-components.
# The --configuration production flag applies production optimizations.

# Verify the output structure
ls -l dist/my-ui-components
# Expected: bundles (fesm2022, esm2022, umd), package.json, typings (.d.ts).

Packaging & Distribution (NPM)

Prepare the library for publishing to a public or private npm registry, correctly managing peerDependencies.

// projects/my-ui-components/package.json
{
  "name": "@my-scope/my-ui-components", // Scoped package name for npm
  "version": "0.1.0", // Semantic versioning (major.minor.patch)
  "peerDependencies": { // Critical: Dependencies that the CONSUMING app must provide
    "@angular/common": "^18.0.0", // Match Angular version of the library
    "@angular/core": "^18.0.0"
  },
  "dependencies": { // Only include truly internal, non-Angular dependencies
    "tslib": "^2.3.0" // Angular libraries typically depend on tslib
  },
  "sideEffects": false // Important for tree-shaking (all code is pure)
}
# Publish to npm (after building the library)
cd dist/my-ui-components
# Navigate to the built library's output directory.

npm publish --access public
# Publishes the package to the npm registry.
# --access public: Required for public packages. Omit for private packages.
# Ensure you are logged in to npm (npm login) or have configured a private registry.

Dependency Management & Tree-shaking

Properly define dependencies, devDependencies, and peerDependencies in package.json. Use sideEffects: false to enable effective tree-shaking.

// projects/my-ui-components/package.json (revisited)
{
  "name": "@my-scope/my-ui-components",
  "version": "0.1.0",
  "peerDependencies": {
    "@angular/common": "^18.0.0", // Consuming app must provide Angular common module
    "@angular/core": "^18.0.0"   // Consuming app must provide Angular core module
  },
  "dependencies": {
    "tslib": "^2.3.0" // Only actual runtime dependencies *not* provided by Angular itself
  },
  "devDependencies": {
    "@angular/compiler": "^18.0.0", // Used only during compilation, not runtime
    "@angular/compiler-cli": "^18.0.0",
    "ng-packagr": "^18.0.0", // Tool for building Angular libraries
    "typescript": "~5.4.0"
  },
  "sideEffects": false, // Crucial for tree-shaking. Tells bundlers that this package has no side effects.
                       // Allows removal of unused exports during bundling.
  "repository": {
    "type": "git",
    "url": "https://github.com/my-org/my-ui-components.git"
  },
  "keywords": ["angular", "ui", "components"],
  "license": "MIT"
}

Versioning & Build Order

Adopt Semantic Versioning (SemVer) for your library releases. In a monorepo, define scripts to manage build order if libraries depend on each other.

# Update library version (SemVer)
cd projects/my-ui-components
npm version patch --no-git-tag-version
# Increments the patch version (e.g., 0.1.0 -> 0.1.1).
# --no-git-tag-version: Prevents npm from creating a git tag, useful in CI/CD.
# Other options: minor, major.

# Example package.json scripts for monorepo build order
// package.json (root workspace)
{
  "name": "my-ui-workspace",
  "version": "0.0.0",
  "scripts": {
    "build:my-ui-components": "ng build my-ui-components --configuration production",
    "build:my-app": "ng build my-app --configuration production",
    "build:all": "npm run build:my-ui-components && npm run build:my-app",
    "test:my-ui-components": "ng test my-ui-components",
    "test:my-app": "ng test my-app"
  }
}
# Execute: npm run build:all
# This ensures the library is built before the application that consumes it.

Consuming the Library (Various Scenarios)

Integrate your UI library into Angular applications using different methods based on your project setup.

The Angular CLI automatically configures tsconfig.json paths for libraries within the same workspace.

// apps/my-app/src/app/app.module.ts (or app.component.ts if standalone)
import { Component } from '@angular/core';
import { ButtonComponent } from '@my-scope/my-ui-components'; // Import directly from library name
import { CardComponent } from '@my-scope/my-ui-components';

@Component({
  selector: 'app-root',
  template: `
    <h1>My App</h1>
    <mui-button (onClick)="showAlert()">Click Me</mui-button>
    <mui-card header="Welcome Card" [elevation]="2">
      <p>This is content inside the card.</p>
    </mui-card>
  `,
  standalone: true,
  imports: [ButtonComponent, CardComponent] // Import standalone components
})
export class AppComponent {
  showAlert() {
    alert('Button clicked!');
  }
}

// tsconfig.json (root workspace) - CLI handles this automatically
{
  "compilerOptions": {
    "paths": {
      "@my-scope/my-ui-components": [
        "dist/my-ui-components"
      ],
      "@my-scope/my-ui-components/*": [
        "dist/my-ui-components/*"
      ]
    }
  }
}

2. Local Linking (Development)

For developing an application that consumes a library not in the same workspace, npm link creates symlinks.

# In the library's dist directory (after ng build)
cd dist/my-ui-components
npm link
# Creates a global symlink for your library.

# In the consuming application's root directory
cd ../../apps/my-other-app # Example: outside the monorepo
npm link @my-scope/my-ui-components
# Links the global symlink to this application's node_modules.

# To unlink later
cd apps/my-other-app
npm unlink @my-scope/my-ui-components
cd dist/my-ui-components
npm unlink

3. Tarball Install (Testing/Private Distribution)

Install a .tgz package directly, useful for testing specific builds or private distribution without a registry.

# In the library's dist directory (after ng build)
cd dist/my-ui-components
npm pack
# Creates a tarball, e.g., my-scope-my-ui-components-0.1.0.tgz

# In the consuming application's root directory
cd ../../apps/my-other-app
npm install ../../dist/my-ui-components/my-scope-my-ui-components-0.1.0.tgz
# Installs the tarball. Update path and version as needed.

4. Private NPM Registry

Install from a private registry after publishing.

# In the consuming application's root directory
npm install @my-scope/[email protected]
# Ensure your npm client is configured to access the private registry.
# (e.g., via .npmrc file or npm login)

CI/CD Integration & Optimization

Automate library builds, tests, and integrate into application deployments. Optimize for production.

// package.json (root workspace) - Example CI/CD scripts
{
  "name": "my-ui-workspace",
  "version": "0.0.0",
  "scripts": {
    "build:lib": "ng build my-ui-components --configuration production",
    "test:lib": "ng test my-ui-components --watch=false --browsers=ChromeHeadless",
    "lint:lib": "ng lint my-ui-components",
    "publish:lib": "cd dist/my-ui-components && npm publish --access public", // Or private
    "build:app": "ng build my-app --configuration production",
    "test:app": "ng test my-app --watch=false --browsers=ChromeHeadless",
    "lint:app": "ng lint my-app",
    "ci:build-and-test": "npm run build:lib && npm run test:lib && npm run build:app && npm run test:app",
    "ci:deploy": "npm run build:app && firebase deploy" // Example deployment
  }
}
// angular.json (for application build optimization)
{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "optimization": true, // Enables various optimizations (minification, tree-shaking)
            "outputHashing": "all", // Cache busting for deployed files
            "sourceMap": false, // Disable for production builds
            "namedChunks": false, // Smaller bundles, less readable names
            "extractLicenses": true, // Extract third-party licenses
            "vendorChunk": false, // Consolidate vendor code into main bundle
            "buildOptimizer": true, // Advanced optimizations for smaller bundles
            "budgets": [ // Define performance budgets to prevent regressions
              {
                "type": "initial",
                "maximumWarning": "500kb",
                "maximumError": "1mb"
              },
              {
                "type": "anyComponentStyle",
                "maximumWarning": "2kb",
                "maximumError": "4kb"
              }
            ]
          },
          "configurations": {
            "production": {
              "aot": true, // Always enable Ahead-of-Time compilation for production
              "serviceWorker": false, // Enable if PWA is desired
              "fileReplacements": [
                {
                  "replace": "apps/my-app/src/environments/environment.ts",
                  "with": "apps/my-app/src/environments/environment.prod.ts"
                }
              ]
            }
          }
        }
      }
    }
  }
}

Quick Reference

  • ng generate library <name>: Creates a new library project.
  • public-api.ts: Defines the public interface of your library. All exports from here are part of the consumable API.
  • @Input() / @Output(): Decorators for component communication.
  • @HostBinding() / @HostListener(): Interact with the host element.
  • peerDependencies: Crucial in package.json for libraries; specifies dependencies the consumer must provide.
  • sideEffects: false: In package.json, enables bundlers to perform aggressive tree-shaking.
  • ng build <library-name> --configuration production: Builds the library with production optimizations.
  • npm link: For local development linking of libraries.
  • npm publish: Publishes library to npm registry.
  • optimization: true, aot: true: Key angular.json build options for production performance.
  • Angular v18.x: Current stable version as of 2026-02-10, with increased adoption of standalone components and signals.

References

  1. Angular Libraries Guide
  2. Angular CLI Workspace and Project File Structure
  3. Angular CLI generate library Command
  4. Angular Best Practices
  5. npm package.json peerDependencies

This page is AI-assisted. References official documentation.