A powerful Angular component library for building rich, validated forms.
Created with ❤️ by Cynthion
- Features
- Installation
- Quick Start
- Core Directives
- Field Decorator
- Field Components
- Theming & Styles
- Root-Level / Cross-Field Validation
- Keyboard Navigation
- Masking
- Extending with Custom Components / Options
- Contributing
- License
ngx-fromidable is a comprehensive Angular component and directive library designed to simplify the creation of rich, validated forms. It provides a wide range of features that enhance form development:
| • Simple directives to define form behavior • Automatically wire model, frame, and validation • Streams for value, validity, dirty state, and errors | • Per-Field or Cross-Field / Root-Level
• Powered by  | • Input / Textarea
• Select / Dropdown / Autocomplete
• Radio Groups / Checkboxes
• Date Picker / Time
• Re-usable  | 
| • Label / Tooltip / Prefix / Suffix
• Floating label transitions
• Forwards  | • Overridable  | • Simple navigation ( | 
| • Powered by  | • Deep-required  | •  | 
Explore and play with live examples on our GitHub Pages: 🌐 https://cynthion.github.io/ngx-formidable/
Install the package and its peer dependencies:
npm install ngx-formidable vest pikaday date-fns ngx-mask// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideNgxFormidable } from 'ngx-formidable';
bootstrapApplication(AppComponent, {
  providers: [...provideNgxFormidable()]
}).catch(console.error);// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { NgxFormidableModule } from 'ngx-formidable';
@NgModule({
  imports: [BrowserModule, NgxFormidableModule.forRoot()],
  bootstrap: [AppComponent]
})
export class AppModule {}- Define your model, form model, frame, and Vest validation suite:
import { enforce, mode, Modes, only, StaticSuite, staticSuite, test } from 'vest';
import { DeepPartial, DeepRequired } from 'ngx-formidable';
export interface User {
  name: string;
  hobby: 'reading' | 'gaming' | 'swimming' | 'other';
  birthdate: Date;
}
export type UserFormModel = DeepPartial<User>;
export type UserFormFrame = DeepRequired<UserFormModel>;
export const userFormModel: UserFormModel = {
  // set initial values here, if any
  name: undefined, // e.g., 'Cynthion',
  hobby: undefined, // e.g., 'reading',
  birthdate: undefined // e.g., new Date(1989, 5, 29),
};
export const userFormFrame: UserFormFrame = {
  name: '',
  hobby: 'other',
  birthdate: new Date()
};
export const userFormValidationSuite = staticSuite((model: UserFormModel, field?: string) => {
  mode(Modes.ALL); // or use Modes.EAGER to just use first
  if (field) only(field);
  test('name', 'First name is required.', () => {
    enforce(model.name).isNotBlank();
  });
  test('name', 'First name does not start with A.', () => {
    enforce(model.name?.toLowerCase()).startsWith('a');
  });
  // further Vest validators
});- Setup your form template:
<form
  formidableForm
  [formValue]="userFormModel"
  [formFrame]="userFormFrame"
  [suite]="userFormValidationSuite"
  [validationOptions]="{ debounceValidationInMs: 200 }"
  (formValueChange$)="userFormModel = $event"
  (validChange$)="isValid = $event"
  (dirtyChange$)="isDirty = $event"
  (errorsChange$)="errors = $event"
  (ngSubmit)="onSubmit()">
  <formidable-field-decorator>
    <formidable-input-field
      formidableFieldErrors
      name="name"
      ngModel
      placeholder="Name"></formidable-input-field>
    <div formidableFieldLabel>Name</div>
    <div formidableFieldTooltip>Enter your name</div>
  </formidable-field-decorator>
  <formidable-field-decorator>
    <formidable-select-field
      placeholder="Select..."
      name="hobby"
      [disabled]="false"
      [readonly]="false"
      [ngModel]="vm.formValue.hobby"
      [options]="hobbyOptions">
      <!-- optional inline options -->
      <formidable-field-option [value]="'gardening'">Gardening</formidable-field-option>
    </formidable-select-field>
    <div
      formidableFieldLabel
      [isFloating]="true">
      Hobby
    </div>
  </formidable-field-decorator>
  <formidable-field-decorator>
    <formidable-date-field
      name="birthdate"
      ngModel
      [minDate]="minDate"
      [maxDate]="maxDate"
      [unicodeTokenFormat]="'dd.MM.yyyy'"></formidable-date-field>
    <div formidableFieldLabel>Birthdate</div>
  </formidable-field-decorator>
  <button
    type="submit"
    [disabled]="!isValid">
    Submit
  </button>
</form>- Binds your form model, frame, and Vest suite.
- Emits formValueChange$,errorsChange$,dirtyChange$,validChange$.
Adds a root-level async validator for cross-field Vest tests on ROOT_FORM.
Renders a <formidable-field-errors> component next to any control to display its validation messages.
Hooks into each ngModel to run per-field async Vest tests.
Hooks into ngModelGroup to validate nested groups.
Wrap any field in a to project:
- Label: <div formidableFieldLabel [isFloating]="true">…</div>
- Tooltip: <div formidableFieldTooltip>…</div>
- Prefix: <div formidableFieldPrefix>…</div>
- Suffix: <div formidableFieldSuffix>…</div>
The decorator adjusts padding and forwards the wrapped field’s properties and events.
| Category | Component | Description | 
|---|---|---|
| Basic Fields | <formidable-input-field> | A standard single-line text input field. | 
| <formidable-textarea-field> | A multi-line textarea with optional autosizing. | |
| Option Fields | <formidable-select-field> | A styled dropdown based on the native <select>. | 
| <formidable-dropdown-field> | A custom dropdown overlay with keyboard support. | |
| <formidable-autocomplete-field> | A text input that filters and suggests options. | |
| <formidable-field-option> | Defines an individual option for any option field. | |
| field groups | <formidable-radio-group-field> | A keyboard-navigable group of radio options. | 
| <formidable-checkbox-group-field> | A keyboard-navigable group of checkboxes. | |
| Date & Time | <formidable-date-field> | A masked date input with a calendar popup. | 
| <formidable-time-field> | A masked time-only input field. | 
Various styling variables allow to customize the theming. Override any supported CSS variable. You can also tweak Pikaday CSS.
// styles.scss
@use 'ngx-formidable';
// ngx-formidable overrides
:root {
  --formidable-field-height: 50px;
  --formidable-color-validation-error: pink;
  --formidable-color-field-background: #d18fe9ff;
  --formidable-color-field-option-background-highlighted: #aa40ed2d;
  --formidable-date-field-panel-width: 200px;
  // add more
}
// Pikaday style overwrites
.pika-lendar {
  background-color: #8a2b75ff;
  // add more
}| CSS Variable | Description | 
|---|---|
| Font Sizes & Line-Heights | |
| --formidable-field-font-size | Base font size for form field text. | 
| --formidable-field-font-weight | Font weight for form field text. | 
| --formidable-field-line-height | Line height for form field text. | 
| --formidable-label-font-size | Font size for labels. | 
| --formidable-label-font-weight | Font weight for labels. | 
| --formidable-label-line-height | Line height for labels. | 
| --formidable-label-floating-font-size | Font size for floating labels. | 
| --formidable-label-floating-font-weight | Font weight for floating labels. | 
| --formidable-label-floating-line-height | Line height for floating labels. | 
| --formidable-field-validation-error-font-size | Font size for validation error messages. | 
| --formidable-field-validation-error-font-weight | Font weight for validation error messages. | 
| --formidable-field-validation-error-line-height | Line height for validation error messages. | 
| --formidable-length-indicator-font-size | Font size for the textarea length indicator. | 
| --formidable-length-indicator-font-weight | Font weight for the textarea length indicator. | 
| --formidable-length-indicator-line-height | Line height for the textarea length indicator. | 
| Field Dimensions | |
| --formidable-field-before-margin-bottom | Vertical margin below each field container. | 
| --formidable-field-border-thickness | Thickness of field borders. | 
| --formidable-field-border-radius | Border-radius for field corners. | 
| --formidable-field-group-border-thickness | Thickness of field group borders. | 
| --formidable-field-group-border-radius | Border-radius for field group corners. | 
| --formidable-label-height | Computed height of the label text line box. | 
| --formidable-field-height | Default height for single-line fields. | 
| --formidable-label-floating-offset | Vertical offset applied when a label floats above its field. | 
| --formidable-field-group-option-padding | Padding of options within a field group. | 
| Colors | |
| --formidable-color-validation-error | Text color for validation errors. | 
| --formidable-color-field-text | Text color for fields. | 
| --formidable-color-field-group-text | Text color for field groups. | 
| --formidable-color-field-text-readonly | Overrides --formidable-color-field-textand--formidable-color-field-group-textwhen field is readonly. | 
| --formidable-color-field-text-disabled | Overrides --formidable-color-field-textand--formidable-color-field-group-textwhen field is disabled. | 
| --formidable-color-field-label | Text color for labels. | 
| --formidable-color-field-label-floating | Text color for floating labels. | 
| --formidable-color-field-tooltip | Text color for tooltip text. | 
| --formidable-color-field-placeholder | Text color for placeholder text. | 
| --formidable-color-field-selection | Background color for selected text. | 
| --formidable-color-field-border | Border color for fields. | 
| --formidable-color-field-border-focus | Border color for fields that are focused. | 
| --formidable-color-field-border-readonly | Overrides --formidable-color-field-borderwhen field is readonly. | 
| --formidable-color-field-border-disabled | Overrides --formidable-color-field-borderwhen field is disabled. | 
| --formidable-color-field-group-border | Border color for field groups. | 
| --formidable-color-field-group-border-focus | Border color for field groups that are focused. | 
| --formidable-color-field-group-border-readonly | Overrides --formidable-color-field-group-borderwhen field is readonly. | 
| --formidable-color-field-group-border-disabled | Overrides --formidable-color-field-group-borderwhen field is disabled. | 
| --formidable-color-field-background | Background color for fields. | 
| --formidable-color-field-group-background | Background color for field groups. | 
| --formidable-color-field-background-readonly | Overrides --formidable-color-field-backgroundand--formidable-color-field-group-backgroundwhen field is readonly. | 
| --formidable-color-field-background-disabled | Overrides --formidable-color-field-backgroundand--formidable-color-field-group-backgroundwhen field is disabled. | 
| --formidable-color-field-option-text-readonly | Text color for option items that are readonly. | 
| --formidable-color-field-option-text-disabled | Text color for option items that are disabled. | 
| --formidable-color-field-option-background-selected | Background color for option items that are selected. | 
| --formidable-color-field-option-background-highlighted | Background color for option items that are highlighted. | 
| --formidable-color-field-option-background-hovered | Background color for option items that are hovered. | 
| --formidable-color-field-focus-box-shadow | Box shadow for fields that are focused. | 
| --formidable-color-field-group-focus-box-shadow | Box shadow for field groups that are focused. | 
| Date-Field Panel | |
| --formidable-color-date-field-panel-select | Text color for “Today” / selected date toggle in calendar. | 
| --formidable-color-date-field-panel-select-hovered | Hover color for the “Today” toggle. | 
| --formidable-color-date-field-panel-date-highlighted-text | Text color for highlighted dates inside the calendar. | 
| --formidable-color-date-field-panel-date-highlighted | Background color for highlighted dates. | 
| --formidable-color-date-field-panel-date-hovered | Background color when hovering a date. | 
| --formidable-color-date-field-panel-date-out-of-range | Color for dates outside the min/max range. | 
| --formidable-color-date-field-panel-day-label | Color for weekday labels in the calendar header. | 
| Option Prefix | |
| --formidable-color-option-prefix-outer | Color of the outer ring/square border of a radio/check box group field option item. | 
| --formidable-color-option-prefix-outer-readonly | Overrides --formidable-color-option-prefix-outerwhen option is readonly. | 
| --formidable-color-option-prefix-outer-disabled | Overrides --formidable-color-option-prefix-outerwhen option is disabled. | 
| --formidable-color-option-prefix-outer-selected | Overrides --formidable-color-option-prefix-outerwhen option is selected. | 
| --formidable-color-option-prefix-outer-highlighted | Overrides --formidable-color-option-prefix-outerwhen option is highlighted. | 
| --formidable-color-option-prefix-inner | Color of the inner ring/square of a radio/check box group field option item. | 
| --formidable-color-option-prefix-inner-readonly | Overrides --formidable-color-option-prefix-innerwhen option is readonly. | 
| --formidable-color-option-prefix-inner-disabled | Overrides --formidable-color-option-prefix-innerwhen option is disabled. | 
| --formidable-color-option-prefix-inner-selected | Overrides --formidable-color-option-prefix-innerwhen option is selected. | 
| --formidable-color-option-prefix-inner-highlighted | Overrides --formidable-color-option-prefix-innerwhen option is highlighted. | 
| --formidable-color-option-prefix-background | Background color behind option prefix elements. | 
| Length Indicator | |
| --formidable-color-length-indicator | Text color for the textarea length indicator. | 
| Textarea | |
| --formidable-textarea-min-height | Minimum height for textareas. | 
| --formidable-textarea-max-height | Maximum height for textareas. | 
| --formidable-textarea-padding-top | Top padding for textareas when autosizing is enabled. | 
| Panels | |
| --formidable-panel-background | Background color for dropdown/autocomplete/date panels. | 
| --formidable-panel-box-shadow | Box-shadow for all panels. | 
| --formidable-panel-max-height | Maximum vertical height for panels (before scrolling). | 
| Animations | |
| --formidable-animation-duration | Duration for label/flyout/open/close animations. | 
| --formidable-animation-easing | Easing curve for animations. | 
| --formidable-hover-duration | Transition duration for hover effects. | 
| --formidable-hover-easing | Easing curve for hover transitions. | 
| Z-Index | |
| --formidable-flyout-z-index | z-index applied to dropdown/flyout panels. | 
| --formidable-overlay-z-index | z-index applied to any full-screen overlays. | 
| --formidable-above-overlay-z-index | z-index for elements that must sit above overlays. | 
| Date-Field Panel | |
| --formidable-date-field-panel-width | Fixed width for the date-picker panel. | 
| --formidable-date-field-panel-border-radius | Border-radius for the date-picker panel. | 
| --formidable-date-field-panel-box-shadow | Box-shadow override for the date-picker panel. | 
| Option Prefix Dimensions | |
| --formidable-option-prefix-dimension-outer | Size of the outer circle/box for radio/checkbox prefixes. | 
| --formidable-option-prefix-dimension-inner | Size of the inner indicator for selected radio/checkbox prefixes. | 
Sometimes your form needs rules that depend on more than one field — for example, you might require that both name and birthdate be provided together. You can implement that with a ROOT_FORM–level test in your Vest suite. Here is how to do that:
- Add the formidableRootValidatedirective to your<form>.
- Include a ROOT_FORMtest in your Vest suite.
<form
  formidableForm
  formidableRootValidate
  [formValue]="userFormModel"
  [formFrame]="userFormFrame"
  [suite]="userFormValidationSuite"
  ...>
  <!-- ... -->
</form>import { staticSuite, test, Modes, only, enforce } from 'vest';
import { ROOT_FORM } from 'ngx-formidable';
export const userFormValidationSuite = staticSuite((model: UserFormModel, field?: string) => {
  mode(Modes.ALL);
  if (field) only(field);
  // Root-Level / Cross‐field rule: name AND birthdate must both be filled
  test(ROOT_FORM, 'Please enter both name and birthdate.', () => {
    enforce(!!model.name && !!model.birthdate).isTruthy();
  });
  // ...
});All controls are keyboard-friendly.
- Disabled/readonly fields ignore navigation.
- Panel= Dropdown/Autocomplete/Date overlay.
- Panels close on Escor when focus leaves the field.
| Key | Inputs / Textareas | Select / Dropdown / Autocomplete | Radio / Checkbox Groups | Date Picker | Time Field | 
|---|---|---|---|---|---|
| Tab | Move to next | Close panel (if open), then move | Move to next | Close panel (if open), then move | Move to next | 
| Shift+Tab | Move to previous | Close panel (if open), then move | Move to previous | Close panel (if open), then move | Move to previous | 
| Enter | — | If panel open: choose highlighted option; if closed: — | — | Parse & accept date | Parse & accept time | 
| Esc | — | Close panel | — | Close panel | — | 
| Arrow Down | — | If closed: open panel; if open: next option (wrap) | Next option | Next day/week | — | 
| Arrow Up | — | If open: previous option (wrap) | Previous option | Previous day/week | — | 
| Arrow Left | — | — | — | Previous day/month | — | 
| Arrow Right | — | — | — | Next day/month | — | 
Typing builds a short type-ahead buffer; the first matching option is highlighted.
- Backspace edits the buffer.
- If the panel is closed, typing the first character opens it.
- The buffer auto-clears after a brief pause.
Some fields support input masking. Under the hood this uses ngx-mask, and you can pass (almost) all of its options straight through.
How config is applied:
- Per-field overrides (via [maskConfig])
- App-wide defaults (provided with FORMIDABLE_MASK_DEFAULTS)
- Library fallbacks (sane defaults)
Provide global defaults once in your app:
Standalone Usage
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideNgxFormidable } from 'ngx-formidable';
bootstrapApplication(AppComponent, {
  providers: [
    ...provideNgxFormidable({
      // global defaults for ngx-mask
      globalMaskConfig: { validation: true, dropSpecialCharacters: true }
    })
  ]
}).catch(console.error);Module Usage
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { NgxFormidableModule } from 'ngx-formidable';
@NgModule({
  imports: [
    BrowserModule,
    NgxFormidableModule.forRoot({
      // global defaults for ngx-mask
      globalMaskConfig: { validation: true, dropSpecialCharacters: true }
    })
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}<formidable-input-field
  name="price"
  [mask]="'000.00'"
  [maskConfig]="{ prefix: 'CHF ', decimalMarker: ',' }"
  ngModel>
</formidable-input-field>That’s it: Set a [mask] when you want masking and optionally tweak behavior with [maskConfig].
When you add your own field component (by implementing IFormidableField or IFormidableOptionField and providing it via FORMIDABLE_FIELD/FORMIDABLE_OPTION_FIELD), it immediately gains:
- Async validation via FormModelDirective
- Root-level / cross-field validation if you use formidableRootValidate
- Error rendering simply by adding formidableFieldErrors
- Decorator support — labels, tooltips, prefixes, and suffixes work out of the box
You don’t need any extra wiring; just implement the interface, extend BaseFieldDirective, and register the provider.
import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseFieldDirective, FieldDecoratorLayout, FORMIDABLE_FIELD, IFormidableField } from 'ngx-formidable';
@Component({
  selector: 'custom-color-picker',
  template: `
    <input
      #inputRef
      type="color"
      [value]="value || '#000000'"
      (input)="onNativeInput($event)" />
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomColorPickerComponent),
      multi: true
    },
    {
      provide: FORMIDABLE_FIELD,
      useExisting: forwardRef(() => CustomColorPickerComponent)
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomColorPickerComponent extends BaseFieldDirective implements IFormidableField {
  @ViewChild('inputRef', { static: true }) inputRef!: ElementRef<HTMLInputElement>;
  protected keyboardCallback = null;
  protected externalClickCallback = null;
  protected windowResizeScrollCallback = null;
  protected registeredKeys: string[] = [];
  protected doOnValueChange(): void {
    // No additional actions needed
  }
  protected doOnFocusChange(_isFocused: boolean): void {
    // No additional actions needed
  }
  // #region ControlValueAccessor
  // Called when Angular writes to the form control
  protected doWriteValue(value: string): void {
    this.inputRef.nativeElement.value = value || '#000000';
  }
  // #endregion
  // #region IFormidableField
  get value(): string | null {
    return this.inputRef.nativeElement.value || null;
  }
  get isLabelFloating(): boolean {
    return !this.isFieldFocused && !this.isFieldFilled;
  }
  get fieldRef(): ElementRef<HTMLElement> {
    return this.inputRef as ElementRef<HTMLElement>;
  }
  decoratorLayout: FieldDecoratorLayout = 'single';
  // #endregion
  // #region Custom Input Properties
  // ...
  // #endregion
  // Called when the native input fires
  onNativeInput(event: Event): void {
    const v = (event.target as HTMLInputElement).value;
    this.valueChangeSubject$.next(v);
    this.valueChanged.emit(v);
    this.onChange(v);
  }
}import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core';
import { FieldOptionComponent, FORMIDABLE_FIELD_OPTION } from 'ngx-formidable';
import { HighlightedEntries } from '../example-form/example-form.model';
@Component({
  selector: 'example-fuzzy-option',
  templateUrl: './example-fuzzy-option.component.html',
  styleUrls: ['./example-fuzzy-option.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [CommonModule],
  providers: [
    {
      // required to provide this component as IFormidableFieldOption
      provide: FORMIDABLE_FIELD_OPTION,
      useExisting: forwardRef(() => ExampleFuzzyOptionComponent)
    }
  ]
})
export class ExampleFuzzyOptionComponent extends FieldOptionComponent {
  @Input() subtitle?: string = 'sub';
  @Input() highlightedEntries?: HighlightedEntries = {
    labelEntries: [],
    subtitleEntries: []
  };
}<div (click)="select ? select() : null">
  <ng-template #contentTemplate>
    <!-- Custom Template -->
    <p class="option-label">
      @if (highlightedEntries?.labelEntries?.length) { @for (entry of highlightedEntries?.labelEntries; track entry.text) {
      <span [class.option-highlight]="entry.isHighlighted">{{ entry.text }}</span>
      } } @else { {{ label }} }
    </p>
    <p class="option-subtitle">
      @if (highlightedEntries?.subtitleEntries?.length) { @for (entry of highlightedEntries?.subtitleEntries; track entry.text) {
      <span [class.option-highlight]="entry.isHighlighted">{{ entry.text }}</span>
      } } @else { {{ subtitle }} }
    </p>
  </ng-template>
</div>:host {
  display: block;
}
.option-label {
  font-weight: normal;
  font-size: 16px;
}
.option-subtitle {
  font-weight: bold;
  font-size: 12px;
}
.option-highlight {
  color: orange;
}Contributions are welcome!
- Fork the repo and create a feature branch.
- Run npm installandnpm run buildto compile.
- Add tests under src/**/*.spec.tsand update existing ones as needed.
- Document any new public APIs or styles in the README.mdand link to the live docs.
- Open a Pull Request describing your changes.
Everything in this repository is licensed under the MIT License unless otherwise specified.
Copyright (c) 2025 - present Christian Lüthold