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.
- Simple component:
<tng-input>for standard fields. - Projected shell:
<tng-form-field>for adornments and inline actions. - Primitive layer:
tngInputwhen 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-slotmarkers 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)
Basic input (Tailwind 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
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
<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
<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
<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, orariaLabelledby. - Link validation copy with
ariaDescribedByorariaErrormessagewhen 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
<tng-input type="email" ariaLabel="Email" [ariaInvalid]="true"></tng-input>
Native browser validation
Native required validation
<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
<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, andEnd. EnterandSpaceare not intercepted by the number input wrapper.input,change,focus,blur,keydown, andkeyupare exposed as component outputs.
Facade events
<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
<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
<!-- 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
<tng-form-field>
<input tngInput />
</tng-form-field>
Incorrect
<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
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);