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

Input

<tng-input> is the default component for a plain single-line field. <tng-form-field> is the projected shell for prefixes, suffixes, and inline actions. tngInput remains the headless primitive underneath both.

Use <tng-input> for the common case. Move to <tng-form-field> only when the field needs projected content that the simple component should not own.

  1. Simple component:<tng-input> for standard fields.
  2. Projected shell:<tng-form-field> for adornments and inline actions.
  3. Primitive layer:tngInput when you need full DOM control.

What you get

  • Native-first behavior:<tng-input> still renders a real native <input>.
  • Clear split of responsibility: plain fields stay on <tng-input>; projected content lives on <tng-form-field>.
  • Input facade: common native attributes and events are forwarded through the component wrapper.
  • Number affordances: number inputs include pointer controls plus keyboard stepping for arrow, page, and boundary keys.
  • Theme contract: shell styling is controlled through tokens such as --tng-input-bg, --tng-input-border, --tng-input-gap, and --tng-input-focus-ring.
  • Stable hooks: the shell and internal control expose data-slot markers and state attrs instead of requiring implementation-specific class names.

Simple examples

Compare the same simple <code>tng-input</code> usage across plain CSS and Tailwind CSS styles.

Basic input (Plain CSS)

Installation

Import the simple component from @tailng-ui/components. Add tngInput, tngPrefix, and tngSuffix only when the field needs projected content through <tng-form-field>.

Recommended imports

ts
import { TngFormFieldComponent, TngInputComponent } from '@tailng-ui/components';
import { TngInput, TngPrefix, TngSuffix } from '@tailng-ui/primitives';

Basic usage

Simple component

Start with <tng-input> whenever the field does not need projected prefix or suffix content.

Default component usage

html
<tng-input type="email" placeholder="team@tailng.dev" ariaLabel="Email"></tng-input>

Projected shell when needed

Move to <tng-form-field> when the field needs projected adornments or inline actions.

Projected shell usage

html
<tng-form-field>
  <span tngPrefix aria-hidden="true">Search</span>
  <input tngInput type="search" placeholder="Search docs" aria-label="Search docs" />
  <span tngSuffix aria-hidden="true">Ctrl+K</span>
</tng-form-field>

Direct primitive attachment

Use direct tngInput attachment when you want full DOM ownership and no component wrapper at all.

Primitive attachment

html
<input tngInput type="email" placeholder="team@tailng.dev" aria-label="Email" />

Structure

<tng-input> owns an internal native <input> and applies the same slot/state contract that the theme already knows how to style. The public styling contract still resolves to [data-slot='input-group'] and [data-slot='input'].

<tng-form-field> uses the same shell contract, but expects a projected input[tngInput] so you can add prefixes, suffixes, and trailing actions.

Accessibility guidance

  • Give the field an accessible name with a visible label, ariaLabel, or ariaLabelledby.
  • Link validation copy with ariaDescribedBy or ariaErrormessage when the message is rendered outside the field.
  • Mark decorative prefix/suffix content as aria-hidden="true".
  • Keep trailing buttons inside <tng-form-field> explicitly labelled.
  • Use the dedicated Textarea page for multiline content instead of stretching the Input API to cover both.

Validation patterns

Stable test path

In component tests and jsdom, prefer the explicit ARIA path when you want deterministic invalid styling.

Stable invalid state

html
<tng-input type="email" ariaLabel="Email" [ariaInvalid]="true"></tng-input>

Native browser validation

Native required validation

html
<tng-input
  type="email"
  ariaLabel="Email"
  ariaErrormessage="email-error"
  pattern="[^@]+@example\.com"
  required
></tng-input>
<p id="email-error">Use your example.com email address.</p>

Mobile and native input hints

Use the native pass-through inputs when the browser can improve editing, validation, or form association.

Native input hints

html
<tng-input
  type="email"
  ariaLabel="Work email"
  placeholder="team@example.com"
  inputmode="email"
  enterkeyhint="next"
  autocapitalize="none"
  autocomplete="email"
  [maxlength]="64"
  [spellcheck]="false"
></tng-input>

Interaction behavior

  • Text input editing, selection, beforeinput, and IME composition remain native.
  • Number inputs handle ArrowUp, ArrowDown, PageUp, PageDown, Home, and End.
  • Enter and Space are not intercepted by the number input wrapper.
  • input, change, focus, blur, keydown, and keyup are exposed as component outputs.

Facade events

html
<tng-input
  type="number"
  ariaLabel="Seats"
  [min]="1"
  [max]="50"
  [step]="1"
  (input)="onInput($event)"
  (change)="onCommit($event)"
  (keydown)="onKeydown($event)"
></tng-input>

Examples

Search as a plain field

Simple search input

html
<tng-input
  type="search"
  placeholder="Search docs"
  ariaLabel="Search docs"
  inputmode="search"
  enterkeyhint="search"
></tng-input>

When to move to form field

As soon as the field needs projected content, use the dedicated Form Field contract.

Move to form field

html
<!-- Move to tng-form-field when the field needs projected content -->
<tng-form-field>
  <span tngPrefix aria-hidden="true">Search</span>
  <input tngInput type="search" placeholder="Search docs" aria-label="Search docs" />
</tng-form-field>

Common pitfalls

Projected shell without the primitive

If you use <tng-form-field>, the projected control still must carry tngInput.

Correct

html
<tng-form-field>
  <input tngInput />
</tng-form-field>

Incorrect

html
<tng-form-field>
  <input />
</tng-form-field>

Testing notes

Query the emitted slot markers in tests instead of relying on wrapper-specific DOM shape. That keeps tests resilient even when the shell implementation changes.

Stable selectors

ts
const input = fixture.nativeElement.querySelector('[data-slot="input"]');
const shell = fixture.nativeElement.querySelector('[data-slot="input-group"]');

expect(input).not.toBeNull();
expect(shell?.hasAttribute('data-focused')).toBe(false);