When the shipped kinds — universal text, template, component plus the per-adapter icon and button kinds (mat-icon / mat-button for Material, bs-icon / bs-button for Bootstrap, prime-icon / prime-button for PrimeNG, ion-icon / ion-button for Ionic) — don't cover your case (a rating widget in the prefix, a status pill in the suffix, a copy-to-clipboard component with bespoke styling), register a custom kind. Two independent steps: a runtime registration (withCustomAddon) and an optional type-level augmentation.

1. Define the addon shape

A custom kind extends BaseAddon and pins kind to a unique string literal:

import type { BaseAddon } from '@ng-forge/dynamic-forms';

export interface RatingAddon extends BaseAddon {
  readonly kind: 'rating';
  readonly value: number;
  readonly max?: number;
}

BaseAddon carries the universal axes — slot, optional hidden, optional className, optional disabled — so you only declare the kind-specific fields.

2. Build the kind component

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import type { RatingAddon } from './rating-addon';

@Component({
  selector: 'app-rating-addon',
  template: `
    <span class="rating">
      @for (i of stars(); track $index) {
        <span [class.filled]="i < addon().value">★</span>
      }
    </span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RatingAddonComponent {
  readonly addon = input.required<RatingAddon>();

  protected readonly stars = computed(() => Array.from({ length: this.addon().max ?? 5 }, (_, i) => i));
}

The contract: declare addon: input.required<TAddon>(). The dispatcher (<df-addon-slot>) wires the addon object via [addon] and forwards the slot HTML attribute on the host element. ARIA defaults are owned by your component — decorative kinds typically set aria-hidden="true"; interactive kinds handle their own labelling.

3. Register at the provider level

Define the AddonKindDefinition next to the addon shape:

import { DynamicFormError, type AddonKindDefinition } from '@ng-forge/dynamic-forms';
import type { RatingAddon } from './rating-addon';

export const RATING_KIND: AddonKindDefinition<RatingAddon> = {
  kind: 'rating',
  loadComponent: () => import('./rating-addon.component').then((m) => m.RatingAddonComponent),
  validate: (addon, fieldKey) => {
    if (typeof addon.value !== 'number' || addon.value < 0) {
      throw new DynamicFormError(`Addon 'rating' on field '${fieldKey}' requires a non-negative 'value'.`);
    }
  },
};

Then pass it through withCustomAddon(...) to provideDynamicForm alongside your adapter s field bundle:

import { ApplicationConfig } from '@angular/core';
import { provideDynamicForm, withCustomAddon } from '@ng-forge/dynamic-forms';
import { withMaterialFields } from '@ng-forge/dynamic-forms-material';
import { RATING_KIND } from './rating-addon';

export const appConfig: ApplicationConfig = {
  providers: [
    provideDynamicForm(...withMaterialFields(), withCustomAddon(RATING_KIND)),
  ],
};

loadComponent returns a Promise — the kind component is loaded lazily on first render and cached.

validate is optional; when provided, the runtime addon validator calls it at config init. Throwing DynamicFormError drops the addon with an actionable warning and the form keeps rendering — validate is a sanitisation hook, not a hard fail.

To make kind: 'rating' autocomplete inside the field s addons array, augment the active adapter s addon-extension seam:

declare module '@ng-forge/dynamic-forms-material' {
  interface MatAddonExtensions {
    'rating': RatingAddon;
  }
}

// Now valid in TS — `kind: 'rating'` is part of the mat-input addon union.
{ type: 'input', key: 'review', addons: [{ slot: 'suffix', kind: 'rating', value: 4 }] }

The runtime registration and the type-level augmentation are independent — use either or both. Without augmentation, custom kinds still work at runtime; you lose IDE narrowing on the addons array.

When not to use a custom kind

  • Static decoration — pure CSS / text → kind: 'text' covers it.
  • An entirely new field control (file picker, rich-text editor, color picker) → register a custom field type, not an addon kind. Addons decorate; field types render the primary control. See Adding custom fields.
  • One-off behavior for a specific button — use action (code-only) or actionRef (registered handler) on a built-in button kind.

Verification checklist

When you ship a custom kind:

  1. The kind component declares addon: input.required<TAddon>().
  2. withCustomAddon(...) is passed to provideDynamicForm after the field-type bundle.
  3. The runtime validate function rejects malformed configs with DynamicFormError.
  4. The type-level augmentation lives in the same file as the runtime registration so the two stay in sync.
  5. ARIA defaults are explicit — aria-hidden="true" for decorative kinds; aria-label for kinds that convey meaning.

Where to next