Ply input-group wraps a label, native input or textarea with ply-input, errors, and helper text. Errors show after the field is touched for both formControlName and [formField]. Composite widgets (combobox, tags, password-input) sit beside it — they are separate CVA controls, not native fields.
Install with npx ply-ui-cli add input-group.
Use ply-input and ply-textarea directives on <input/> and <textarea></textarea> elements wrapped inside <ply-input-group> component. Check example below.
<ply-input-group class="w-[280px]"> <ply-label for="text">Name</ply-label> <input ply-input type="text" placeholder="Name" /> </ply-input-group> <ply-input-group class="w-[280px]"> <ply-label for="text">Description</ply-label> <textarea ply-textarea placeholder="Start type here...">Some text</textarea> </ply-input-group>
TypeScript
import { Component } from '@angular/core';
// Install: npx ply-ui-cli add input-group
// Paths are relative to aliases.components (default: src/app/components)
import { BaseAddonEndDirective } from './input-group/base-addon-end.directive';
import { BaseAddonStartDirective } from './input-group/base-addon-start.directive';
import { BaseInputDirective } from './input-group/base-input.directive';
import { BaseTextareaDirective } from './input-group/base-textarea.directive';
import { ErrorComponent } from './input-group/error/error.component';
import { InfoTextComponent } from './input-group/info-text/info-text.component';
import { InputGroupComponent } from './input-group/input-group.component';
import { LabelComponent } from './input-group/label/label.component';
@Component({
selector: 'app-input-group-example',
imports: [
BaseAddonEndDirective,
BaseAddonStartDirective,
BaseInputDirective,
BaseTextareaDirective,
ErrorComponent,
InfoTextComponent,
InputGroupComponent,
LabelComponent
],
template: `...`
})
export class InputGroupExampleComponent {}Install
npx ply-ui-cli add input-group
Check examples below if you want to use disabled or readonly state of input. The example works with reactive forms as well.
<div class="grid grid-cols-2 flex-col gap-4 items-center justify-center">
<form class="col-span-2 md:col-span-1">
<ply-input-group>
<ply-label for="text">Name</ply-label>
<input ply-input type="text" placeholder="Name" disabled/>
</ply-input-group>
<ply-input-group>
<ply-label for="text">Last Name</ply-label>
<input ply-input type="text" placeholder="Last Name" disabled/>
</ply-input-group>
<ply-input-group>
<ply-label for="text">Description</ply-label>
<textarea ply-textarea placeholder="Start type here..." disabled>Some text</textarea>
</ply-input-group>
</form>
<form class="col-span-2 md:col-span-1">
<ply-input-group>
<ply-label for="text">Readonly</ply-label>
<input ply-input type="text" value="John" readonly />
</ply-input-group>
<ply-input-group>
<ply-label for="text">Readonly</ply-label>
<input ply-input type="text" value="Doe" readonly/>
</ply-input-group>
<ply-input-group>
<ply-label for="text">Description</ply-label>
<textarea ply-textarea placeholder="Start type here..." readonly>Some text</textarea>
</ply-input-group>
</form>
</div>
TypeScript
import { Component } from '@angular/core';
// Install: npx ply-ui-cli add input-group
// Paths are relative to aliases.components (default: src/app/components)
import { BaseAddonEndDirective } from './input-group/base-addon-end.directive';
import { BaseAddonStartDirective } from './input-group/base-addon-start.directive';
import { BaseInputDirective } from './input-group/base-input.directive';
import { BaseTextareaDirective } from './input-group/base-textarea.directive';
import { ErrorComponent } from './input-group/error/error.component';
import { InfoTextComponent } from './input-group/info-text/info-text.component';
import { InputGroupComponent } from './input-group/input-group.component';
import { LabelComponent } from './input-group/label/label.component';
@Component({
selector: 'app-input-group-example',
imports: [
BaseAddonEndDirective,
BaseAddonStartDirective,
BaseInputDirective,
BaseTextareaDirective,
ErrorComponent,
InfoTextComponent,
InputGroupComponent,
LabelComponent
],
template: `...`
})
export class InputGroupExampleComponent {}Install
npx ply-ui-cli add input-group
For validation message use <ply-error>. Bind the native input with Angular 22 [formField] or with formControlName. Messages show after the field is touched.
<form [formRoot]="nameForm">
<ply-input-group>
<ply-label>Name</ply-label>
<input ply-input [formField]="nameForm.firstName" />
<ply-error>{{ firstNameError() }}</ply-error>
</ply-input-group>
</form> Native inputs take [formField] directly. CVA hosts (ply-toggle, ply-custom-select, combobox) use the same binding via Angular’s CVA interop. Full recipe: signal forms cookbook.
TypeScript
import { Component } from '@angular/core';
// Install: npx ply-ui-cli add input-group
// Paths are relative to aliases.components (default: src/app/components)
import { BaseAddonEndDirective } from './input-group/base-addon-end.directive';
import { BaseAddonStartDirective } from './input-group/base-addon-start.directive';
import { BaseInputDirective } from './input-group/base-input.directive';
import { BaseTextareaDirective } from './input-group/base-textarea.directive';
import { ErrorComponent } from './input-group/error/error.component';
import { InfoTextComponent } from './input-group/info-text/info-text.component';
import { InputGroupComponent } from './input-group/input-group.component';
import { LabelComponent } from './input-group/label/label.component';
@Component({
selector: 'app-input-group-example',
imports: [
BaseAddonEndDirective,
BaseAddonStartDirective,
BaseInputDirective,
BaseTextareaDirective,
ErrorComponent,
InfoTextComponent,
InputGroupComponent,
LabelComponent
],
template: `...`
})
export class InputGroupExampleComponent {}Install
npx ply-ui-cli add input-group
Just add <ply-info-text> and it utomatically will appear under the input. Check examples below.
<form>
<ply-input-group class="w-[280px] mb-4">
<ply-label for="text">Add Email</ply-label>
<input ply-input type="text" placeholder="[email protected]" />
<ply-info-text>We'll never share your email. Check our <a href="#" ply-link>Privacy
Policy</a></ply-info-text>
</ply-input-group>
</form>
TypeScript
import { Component } from '@angular/core';
// Install: npx ply-ui-cli add input-group
// Paths are relative to aliases.components (default: src/app/components)
import { BaseAddonEndDirective } from './input-group/base-addon-end.directive';
import { BaseAddonStartDirective } from './input-group/base-addon-start.directive';
import { BaseInputDirective } from './input-group/base-input.directive';
import { BaseTextareaDirective } from './input-group/base-textarea.directive';
import { ErrorComponent } from './input-group/error/error.component';
import { InfoTextComponent } from './input-group/info-text/info-text.component';
import { InputGroupComponent } from './input-group/input-group.component';
import { LabelComponent } from './input-group/label/label.component';
@Component({
selector: 'app-input-group-example',
imports: [
BaseAddonEndDirective,
BaseAddonStartDirective,
BaseInputDirective,
BaseTextareaDirective,
ErrorComponent,
InfoTextComponent,
InputGroupComponent,
LabelComponent
],
template: `...`
})
export class InputGroupExampleComponent {}Install
npx ply-ui-cli add input-group
Run the following command to add this component to your project:
npx ply-ui-cli add input-group
For AI agents
Copy a prompt with the registry name, CLI install, and import — or add the Ply MCP server.
npx -y ply-ui-mcp
Ply provides standalone components. Import the exact elements you want to use into your component.
// Install: npx ply-ui-cli add input-group
// Paths are relative to aliases.components (default: src/app/components)
import { BaseAddonEndDirective } from './input-group/base-addon-end.directive';
import { BaseAddonStartDirective } from './input-group/base-addon-start.directive';
import { BaseInputDirective } from './input-group/base-input.directive';
import { BaseTextareaDirective } from './input-group/base-textarea.directive';
import { ErrorComponent } from './input-group/error/error.component';
import { InfoTextComponent } from './input-group/info-text/info-text.component';
import { InputGroupComponent } from './input-group/input-group.component';
import { LabelComponent } from './input-group/label/label.component';
@Component({
selector: 'app-your-component',
imports: [
BaseAddonEndDirective,
BaseAddonStartDirective,
BaseInputDirective,
BaseTextareaDirective,
ErrorComponent,
InfoTextComponent,
InputGroupComponent,
LabelComponent
],
template: `...`
})
export class YourComponent {} API for input-group — generated from JSDoc in the library source.
| API | Member | Type | Default | Description |
|---|---|---|---|---|
ply-error | — | — | — | An error message element for use inside a `ply-input-group`. |
ply-error | class | string | '' | Additional host CSS classes (merged via cn()). |
ply-info-text | — | — | — | A helper/info text element for use inside a `ply-input-group`. |
ply-info-text | class | string | '' | Additional host CSS classes (merged via cn()). |
ply-input-group | class | string | '' | Additional host CSS classes (merged via cn()). |
ply-input-group | invalid | boolean | false | Force the error state (red border + projected `ply-error`). Composite CVA hosts such as `ply-password-input` bind this when the form control lives on the wrapper, not the projected native input. |
ply-label | class | string | '' | Additional host CSS classes (merged via cn()). |
ply-label | for | string | '' | Explicit `for` attribute; `ply-input-group` auto-fills it via . |
[ply-addon-end] | — | — | — | An element positioned at the end of an input group. |
[ply-addon-start] | — | — | — | An element positioned at the start of an input group. |
[ply-input] | — | — | — | A standard input directive that applies consistent Ply styling to native text inputs. Extra `class` values are merged with `cn()`. Supports disabled and readonly states. Focus chrome lives on `ply-input-group` (`FOCUS_RING_WITHIN`) so addons stay inside the ring. |
[ply-input] | class | string | '' | Extra classes merged via `cn()`. |
[ply-textarea] | — | — | — | A standard textarea directive that applies consistent Ply styling. Extra `class` values are merged with `cn()`. Focus chrome lives on `ply-input-group`. |
[ply-textarea] | class | string | '' | Extra classes merged via `cn()`. |
Content