Building A Reusable Angular UI Component Module

A well-designed Angular component library can turn repeated interface work into a dependable foundation for several applications. Instead of rebuilding buttons, form controls, alerts and data displays for each project, a team can package them behind a clear public API and maintain them in one place.

An Angular module provides a useful boundary for this kind of work. It can declare related components, import the Angular features they need, and export only the pieces that other parts of an application should use. Although standalone components are now a strong option in modern Angular, NgModules remain relevant in established applications, shared libraries and codebases that still use module-based architecture.

The most valuable result is not simply less code. A reusable UI module creates consistent behaviour, styling and accessibility across a product. For a team working across Sydney, Melbourne or Brisbane projects, that consistency can make handovers easier and reduce the risk of small interface differences appearing between customer-facing screens.

Define The Module’s Responsibility

Start by deciding what belongs together. A shared module should have a focused purpose, such as buttons and form controls, navigation elements, feedback messages, or a collection of brand-specific visual components. Putting every reusable component into one large SharedModule often creates unclear dependencies and encourages unrelated features to become tightly connected.

A useful boundary is based on how components are consumed. A UiModule might contain UiButtonComponent, UiCardComponent, UiAlertComponent and UiSpinnerComponent because those controls form a common visual language. A separate UserModule could contain profile-specific screens and workflows that are reusable only within the user area.

Before writing code, list the module’s public components, required Angular directives and external dependencies. This simple inventory exposes problems early. If a supposedly small module needs half the application to compile, its scope is probably too broad. The module should provide UI building blocks, not business rules, HTTP calls or application-specific state.

Create A Clear Angular Module

A traditional Angular module is defined with the @NgModule decorator. Components belong in declarations, Angular features and third-party modules belong in imports, and components intended for use elsewhere belong in exports.

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiAlertComponent } from './ui-alert/ui-alert.component';
import { UiButtonComponent } from './ui-button/ui-button.component';
import { UiCardComponent } from './ui-card/ui-card.component';

@NgModule({
  declarations: [
    UiAlertComponent,
    UiButtonComponent,
    UiCardComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    UiAlertComponent,
    UiButtonComponent,
    UiCardComponent
  ]
})
export class UiModule {}

CommonModule supplies commonly used directives and pipes for feature modules, including ngIf, ngFor and related functionality. The root application module traditionally imports BrowserModule instead. Keeping those responsibilities separate helps prevent confusing browser-specific dependencies from spreading into every feature.

The exports array is the module’s public surface. A component can be declared internally without being exported, which is useful for implementation details. Export only what consumers need. A small API is easier to document, test and change than a module that exposes every helper component and internal directive.

Design Components With Stable APIs

Reusable components should communicate through inputs and outputs rather than reaching into the parent component’s data or services. Inputs describe configuration, while outputs report user actions. This keeps the component portable across pages and applications.

import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'ui-button',
  template: `
    <button
      type="button"
      [disabled]="disabled"
      [attr.aria-busy]="busy"
      (click)="pressed.emit()">
      <span *ngIf="busy">Saving…</span>
      <ng-content *ngIf="!busy"></ng-content>
    </button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UiButtonComponent {
  @Input() disabled = false;
  @Input() busy = false;
  @Output() pressed = new EventEmitter<void>();
}

A consumer can now use the component without knowing how its template is implemented:

<ui-button [busy]="isSaving" [disabled]="form.invalid" (pressed)="save()">
  Save changes
</ui-button>

Choose input names that describe intent and avoid leaking internal terminology. For example, busy communicates a visual state more clearly than showLoadingIcon. Consider projected content with ng-content when consumers need to provide labels or icons. This produces a flexible component without creating a large collection of text-related inputs.

In an Australian product, labels and validation messages may need to support Australian English, regional terminology and different date conventions. Keep user-facing strings configurable rather than embedding assumptions into the component. A reusable control should also work for customers in Perth and Adelaide without requiring a fork of the component library.

Organise Templates, Styles And Assets

A component library needs a predictable file structure. Keep each component’s TypeScript, template, styles and tests together, then group related components under a module directory.

src/app/ui/
  ui.module.ts
  ui-button/
    ui-button.component.ts
    ui-button.component.html
    ui-button.component.scss
    ui-button.component.spec.ts
  ui-alert/
    ui-alert.component.ts
    ui-alert.component.html
    ui-alert.component.scss
    ui-alert.component.spec.ts

Use selectors with a consistent prefix such as ui- to avoid collisions with application components and third-party libraries. Scope styles carefully. A component library should avoid global selectors such as button { ... } unless the project deliberately maintains a global design system. Component-level styles reduce the chance that a reusable control unexpectedly changes a checkout page or administration screen.

Shared design tokens can be stored in Sass variables, CSS custom properties or a dedicated theme file. Colours, spacing, typography and border radii should have names that describe their role, such as --ui-colour-primary or --ui-space-medium. This makes branding changes safer when a client moves from a cool technology palette to the stronger colours common in Australian retail and government interfaces.

Assets need the same discipline. If a component depends on an icon set or font, document how that dependency is installed and loaded. Avoid quietly importing large libraries into every bundle when an application uses only a few icons. Bundle size matters for users on mobile connections, including commuters travelling through areas with inconsistent coverage outside major city centres.

Handle Forms And Accessibility Properly

Form controls are among the most useful reusable components, but they are also easy to design badly. A custom input should support labels, error messages, disabled states, keyboard interaction and a meaningful relationship between the input and its description. The visual appearance is only one part of the component’s responsibility.

For complex controls, implement Angular’s ControlValueAccessor interface so the component works with reactive forms and template-driven forms. This allows a reusable date picker, toggle or search box to participate in validation and form state in the same way as a native input.

@Component({
  selector: 'ui-toggle',
  template: `
    <button
      type="button"
      role="switch"
      [attr.aria-checked]="value"
      (click)="toggle()">
      <ng-content></ng-content>
    </button>
  `
})
export class UiToggleComponent {
  value = false;

  toggle(): void {
    this.value = !this.value;
  }
}

This simplified example still needs focus styling, disabled handling and value propagation before it is suitable for a real form. Test with keyboard-only navigation and a screen reader rather than relying on visual inspection. Australian organisations frequently need to meet accessibility expectations under the Disability Discrimination Act, and accessible components are easier to reuse than controls that require retrofitting later.

Use semantic HTML whenever possible. A native button is preferable to a clickable div, and a real label is preferable to a text span positioned beside an input. ARIA attributes should clarify behaviour, not compensate for avoidable choices of inappropriate elements.

Manage Dependencies And Public Exports

A reusable module should import only what its components require. Importing a large application-specific module may work during development, but it hides dependencies and can create circular references. Keep low-level UI code independent from feature modules such as orders, accounts or reporting.

If the library uses Angular Material, CDK utilities or another component toolkit, decide whether those dependencies are part of the module’s contract. A component that relies on Material should either expose a documented peer dependency or wrap the dependency so consumers do not need to understand its internals. The choice affects installation, upgrades and bundle size.

Barrel files can make imports convenient:

export * from './ui.module';
export * from './ui-button/ui-button.component';
export * from './ui-alert/ui-alert.component';

Use them carefully. Exporting internal helpers from a barrel can make them appear officially supported when they are not. A public export should be treated as a commitment. Once several applications depend on a class or component, removing or renaming it becomes a migration task.

For teams maintaining products across Australian time zones, clear dependency boundaries also help release coordination. A team in Melbourne can prepare a module update while a Perth-based team reviews compatibility, provided the module has a stable API and documented versioning rules.

Test Behaviour Rather Than Appearance Alone

Component tests should verify the behaviour a consumer relies on. For a button, test disabled and busy states, emitted events and projected content. For an alert, test its role, message and dismiss action. For a form control, test value changes, validation state and keyboard interaction.

it('emits pressed when the button is clicked', () => {
  const fixture = TestBed.createComponent(UiButtonComponent);
  const component = fixture.componentInstance;
  const pressed = jasmine.createSpy('pressed');

  component.pressed.subscribe(pressed);
  fixture.detectChanges();

  fixture.nativeElement.querySelector('button').click();

  expect(pressed).toHaveBeenCalled();
});

Add integration tests where components interact with Angular forms or content projection. A component may pass an isolated unit test but fail when placed inside a reactive form, rendered repeatedly in a list or used with an internationalised label.

Visual regression testing can complement unit tests for a design system. Capture important states such as hover, focus, error, loading and mobile layouts. Do not treat screenshots as the only authority; a page can look correct while having broken tab order or missing accessible names.

Before publishing a module, run linting, type checking, unit tests and a production build. Angular compiler errors often reveal incorrect declarations or missing exports that a local development session can conceal.

Publish And Evolve The Library

When several applications consume the same UI module, move it into a library with a deliberate package structure. Angular CLI can generate a workspace library, while tools such as Storybook can document components interactively. A component catalogue gives developers a place to inspect states without navigating through an entire business application.

Use semantic versioning to communicate change. A new optional input is usually a minor release, while removing an exported component or changing an input’s meaning is a major release. Keep a changelog with migration notes and show replacement examples when an API changes.

Modern Angular projects may prefer standalone components, and a library can support that direction while still serving NgModule applications. Standalone components can be imported directly, while a compatibility module can group and export them for older consumers. Choose a migration path based on the applications that use the library rather than changing architecture solely for fashion.

A useful technical blog such as Kilt and Code can provide a place to record these decisions, especially when a team needs to explain why a module uses a particular dependency or migration strategy. Documentation should cover installation, supported Angular versions, examples, accessibility expectations and known limitations.

A reusable library is successful when application developers can adopt it without reading its source code. Name components consistently, show realistic examples, and state which styles or themes must be loaded. Good documentation reduces support requests and protects the original design decisions as the team grows.

Create a small UiModule with two or three well-tested components, define its public exports, and use it in a real feature before expanding its scope. Treat every exported component as a maintained product: give it an accessible API, test its important states, document its dependencies and version changes carefully. That approach turns repeated Angular markup into a reliable shared foundation for applications across Australia and beyond.