Input
Styled input, textarea, label and input group with addons.
import { โฆ } from '@inlaylab/ivorie/input'import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
@Component({
selector: 'demo-input',
imports: [IV_INPUT_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="grid w-full max-w-sm gap-2">
<label ivLabel for="input-email">Email</label>
<input ivInput id="input-email" type="email" placeholder="you@example.com" />
</div>
`,
})
export default class InputDemo {}
Installation
npx ivorie add inputimport { โฆ } from '@inlaylab/ivorie/input'Plain directives on native elements. Template-driven and reactive forms work unchanged.
Usage
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';<label ivLabel for="email">Email</label>
<input ivInput id="email" type="email" />Examples
Disabled and invalid
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
@Component({
selector: 'demo-input-states',
imports: [IV_INPUT_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="grid w-full max-w-sm gap-3">
<input ivInput placeholder="Disabled" disabled aria-label="Disabled" />
<input ivInput placeholder="Invalid" aria-invalid="true" aria-label="Invalid" />
</div>
`,
})
export default class InputStatesDemo {}
Textarea
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
@Component({
selector: 'demo-input-textarea',
imports: [IV_INPUT_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="grid w-full max-w-sm gap-2">
<label ivLabel for="input-bio">Bio</label>
<textarea ivTextarea id="input-bio" rows="4" placeholder="Tell us about yourself"></textarea>
</div>
`,
})
export default class InputTextareaDemo {}
Input group icon
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
import { LucideSearch } from '@lucide/angular';
@Component({
selector: 'demo-input-group-icon',
imports: [IV_INPUT_IMPORTS, LucideSearch],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-input-group class="w-full max-w-sm">
<iv-input-group-addon>
<svg lucideSearch></svg>
</iv-input-group-addon>
<input ivInputGroupInput placeholder="Search users" aria-label="Search users" />
</iv-input-group>
`,
})
export default class InputGroupIconDemo {}
Input group button
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
import { LucideEye } from '@lucide/angular';
import { LucideEyeOff } from '@lucide/angular';
@Component({
selector: 'demo-input-group-button',
imports: [IV_INPUT_IMPORTS, LucideEye, LucideEyeOff],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-input-group class="w-full max-w-sm">
<input ivInputGroupInput [type]="show() ? 'text' : 'password'" placeholder="Password" aria-label="Password" />
<iv-input-group-addon align="inline-end">
<iv-input-group-button size="icon-xs" aria-label="Toggle password" (click)="show.set(!show())">
@if (show()) {
<svg lucideEyeOff></svg>
} @else {
<svg lucideEye></svg>
}
</iv-input-group-button>
</iv-input-group-addon>
</iv-input-group>
`,
})
export default class InputGroupButtonDemo {
protected readonly show = signal(false);
}
Input group text
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
@Component({
selector: 'demo-input-group-text',
imports: [IV_INPUT_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-input-group class="w-full max-w-sm">
<iv-input-group-addon>
<iv-input-group-text>https://</iv-input-group-text>
</iv-input-group-addon>
<input ivInputGroupInput placeholder="example" aria-label="Domain" />
<iv-input-group-addon align="inline-end">
<iv-input-group-text>.app</iv-input-group-text>
</iv-input-group-addon>
</iv-input-group>
`,
})
export default class InputGroupTextDemo {}
Block addon
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
@Component({
selector: 'demo-input-group-block',
imports: [IV_INPUT_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-input-group class="w-full max-w-sm">
<input ivInputGroupInput #post (input)="length.set(post.value.length)" maxlength="140" placeholder="What is happening?" aria-label="Post" />
<iv-input-group-addon align="block-end" class="border-t">
<iv-input-group-text>{{ length() }}/140</iv-input-group-text>
<iv-input-group-button variant="default" class="ml-auto">Post</iv-input-group-button>
</iv-input-group-addon>
</iv-input-group>
`,
})
export default class InputGroupBlockDemo {
protected readonly length = signal(0);
}
Group textarea
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_INPUT_IMPORTS } from '@inlaylab/ivorie/input';
import { LucidePaperclip, LucideSmile } from '@lucide/angular';
@Component({
selector: 'demo-input-group-textarea',
imports: [IV_INPUT_IMPORTS, LucidePaperclip, LucideSmile],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-input-group class="w-full max-w-sm">
<textarea
ivInputGroupTextarea
rows="2"
maxlength="280"
placeholder="Message #general"
aria-label="Message"
[value]="message()"
(input)="onInput($event)"
></textarea>
<iv-input-group-addon align="block-end" class="border-t">
<iv-input-group-button size="icon-xs" aria-label="Attach file">
<svg lucidePaperclip></svg>
</iv-input-group-button>
<iv-input-group-button size="icon-xs" aria-label="Add emoji">
<svg lucideSmile></svg>
</iv-input-group-button>
<iv-input-group-text class="ml-auto tabular-nums">{{ message().length }}/280</iv-input-group-text>
<iv-input-group-button variant="default" [disabled]="!message().trim()">Send</iv-input-group-button>
</iv-input-group-addon>
</iv-input-group>
`,
})
export default class InputGroupTextareaDemo {
protected readonly message = signal('');
protected onInput(event: Event) {
this.message.set((event.target as HTMLTextAreaElement).value);
}
}
API
Every part also accepts class.
input[ivInput], textarea[ivTextarea], label[ivLabel]
Directives with no inputs besides class.
iv-input-group
No inputs besides class. Use input[ivInputGroupInput] and textarea[ivInputGroupTextarea] as its controls. They drop their own border and ring so the group draws them.
iv-input-group-addon
| Name | Type | Default | Description |
|---|---|---|---|
align |
'inline-start' | 'inline-end' | 'block-start' | 'block-end' |
'inline-start' |
Position relative to the control. |
iv-input-group-text
No inputs besides class. Muted helper text in an addon.
iv-input-group-button
| Name | Type | Default | Description |
|---|---|---|---|
variant |
'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link' |
'ghost' |
Same variants as ivButton. |
size |
'xs' | 'sm' | 'icon-xs' | 'icon-sm' |
'xs' |
Button size. |
type |
'button' | 'submit' | 'reset' |
'button' |
Native type. |
disabled |
boolean |
false |
Disables the inner button. |
aria-label |
string | null |
null |
Accessible name, set on the inner button. |
Accessibility
iv-input-groupandiv-input-group-addonhaverole="group".- Set
aria-invalid="true"on a control for the destructive border and ring. The group picks it up too. - Pair every control with a
label[ivLabel]andfor/id. - Clicking an addon focuses the group's
input, unless the click is on a button. - Icon-only
iv-input-group-buttonneedsaria-label.
Notes
- Use
ivInputandivTextareafor standalone controls. Do not wrap them. aria-invalidis not set for you. Bind it, for example[attr.aria-invalid]="control.invalid && control.touched".