Getting StartedInstallation and setup guides 6
FormInput and selection components 26
LayoutWorkflow and structural layout components 10
NavigationMenu surfaces and hierarchical actions 7
OverlayModal and floating layer surfaces 3
FeedbackStatus, empty, progress, and loading placeholder patterns 6
UtilityGeneral-purpose interface utilities 7

Number Range

<tng-number-range> renders a dual numeric input group — a min field, a configurable separator, and a max field — inside a single unified shell. It supports controlled and default value modes, Signal Forms custom controls, optional legacy Angular forms adapters, and automatic range validity checking.

What you get

  • Single-shell range field: min and max inputs share one visual border, one focus ring, and one validity state.
  • Native number semantics: both inputs are type="number", so browser autocomplete, keyboard stepping, and validation all work natively.
  • Built-in validity logic: the component computes invalid when min > max, when either value exceeds configured bounds, or when the explicit [invalid] input is set.
  • Forms-ready: implements the Signal Forms value-control contract. Reactive and template-driven forms opt in with tngAngularForms.
  • Slot customization: override CSS classes on any internal slot via the [slot] input.
  • Theme contract: all visual properties are driven by the --tng-number-range-* token surface — no internal DOM targeting needed.
  • State attributes:data-disabled, data-readonly, and data-invalid are reflected to the host and the inner group for container styling.

Simple examples

Compare the same basic <code>tng-number-range</code> usage across plain CSS and Tailwind CSS styles.

Basic range (Plain CSS)

Installation

Import TngNumberRangeComponent from @tailng-ui/components into your standalone component's imports array.

Recommended import

ts
import { TngNumberRangeComponent } from '@tailng-ui/components';

Basic usage

Template usage

Pass [min], [max], and an accessible label. Subscribe to (valueChange) for value updates, or use the full (rangeChange) output when you also need to know which field changed and whether the resulting range is valid.

Basic usage

html
<tng-number-range
  [min]="0"
  [max]="100"
  ariaLabel="Price range"
  minPlaceholder="Min"
  maxPlaceholder="Max"
  (valueChange)="onRangeChange($event)"
></tng-number-range>

Controlled value

Drive the displayed value with [value] and update it through (valueChange). Both min and max can be null to represent an empty field.

Controlled value

ts
import { Component, signal } from '@angular/core';
import { TngNumberRangeComponent } from '@tailng-ui/components';
import type { TngNumberRangeValue } from '@tailng-ui/primitives';

@Component({
  selector: 'app-price-range',
  standalone: true,
  imports: [TngNumberRangeComponent],
  template: `
    <tng-number-range
      [min]="0"
      [max]="1000"
      [value]="range()"
      (valueChange)="range.set($event)"
      ariaLabel="Price range"
    ></tng-number-range>
  `,
})
export class PriceRangeComponent {
  protected readonly range = signal<TngNumberRangeValue>({ min: 100, max: 500 });
}

Default (uncontrolled) value

For uncontrolled usage, provide [defaultValue] once at mount time. The component owns the value internally from that point.

Default value

html
<tng-number-range
  [defaultValue]="{ min: 10, max: 90 }"
  ariaLabel="Quantity range"
></tng-number-range>

Custom separator

The separator between min and max defaults to . Override it with the separator input to use any string or symbol.

Custom separator

html
<!-- Default "—" separator -->
<tng-number-range ariaLabel="Price range"></tng-number-range>

<!-- Custom separator text -->
<tng-number-range separator="to" ariaLabel="Distance range"></tng-number-range>

<!-- Custom separator symbol -->
<tng-number-range separator="↔" ariaLabel="Temperature range"></tng-number-range>

Structure

<tng-number-range> renders a div[role="group"] container that holds two native <input type="number"> elements and a <span> separator. The host element itself carries the data-disabled, data-readonly, and data-invalid state attributes. The inner group element mirrors the same state attributes for container and theme-level styling.

Spinner controls (browser-native step arrows) are hidden by default via CSS so the field looks like a plain text pair. The numeric type is retained so keyboard stepping and mobile numeric keyboards still work.

Accessibility guidance

  • Always provide an accessible name for the group via ariaLabel or ariaLabelledby. This labels the role="group" container.
  • Each individual input has its own label via minAriaLabel (default: "Minimum") and maxAriaLabel (default: "Maximum"). Override these when the context demands more descriptive labels, for example "Minimum price" and "Maximum price".
  • When the range is invalid, both inputs receive aria-invalid="true" automatically. Link an error description by placing it in a sibling element and referencing it at the form-field level.
  • The separator span carries aria-hidden="true" to keep the reading order clean for screen readers.
  • Clicking anywhere in the group shell that is not an input focuses the min input, matching the expected shell click-to-focus pattern.

Validation patterns

The component runs three validity checks automatically: min is at or above the configured lower bound, max is at or below the configured upper bound, and min does not exceed max. It enters the invalid state as soon as any of these fail or when the explicit [invalid] input is set to true.

Validation patterns

html
<!-- Explicit invalid state via input -->
<tng-number-range
  ariaLabel="Price range"
  [invalid]="true"
></tng-number-range>

<!-- Auto-computed: invalid when min > max or bounds exceeded -->
<tng-number-range
  [min]="0"
  [max]="100"
  [value]="{ min: 80, max: 20 }"
  ariaLabel="Price range"
></tng-number-range>

Forms integration

<tng-number-range> binds directly to Angular Signal Forms through its value model. Reactive forms and template-driven forms use the explicit legacy adapter. The control value type is TngNumberRangeValue — an object with min and max fields that are either a number or null.

Legacy reactive forms usage

ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { TngNumberRangeAngularFormsAdapter, TngNumberRangeComponent } from '@tailng-ui/components';

@Component({
  selector: 'app-range-form',
  standalone: true,
  imports: [ReactiveFormsModule, TngNumberRangeComponent, TngNumberRangeAngularFormsAdapter],
  template: `
    <form [formGroup]="form">
      <tng-number-range
        tngAngularForms
        formControlName="priceRange"
        [min]="0"
        [max]="1000"
        ariaLabel="Price range"
      ></tng-number-range>
    </form>
  `,
})
export class RangeFormComponent {
  readonly form = new FormGroup({
    priceRange: new FormControl({ min: 100, max: 500 }),
  });
}

Testing notes

Query the stable CSS class selectors or state attributes rather than relying on implementation-specific DOM shape. Both the root group and the individual min/max inputs expose predictable class names.

Stable selectors

ts
const root = fixture.nativeElement.querySelector('.tng-number-range');
const minInput = fixture.nativeElement.querySelector('.tng-number-range__input--min');
const maxInput = fixture.nativeElement.querySelector('.tng-number-range__input--max');

expect(root).not.toBeNull();
expect(root?.hasAttribute('data-invalid')).toBe(false);
expect(minInput?.value).toBe('100');
expect(maxInput?.value).toBe('500');