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

CLInpm
npx ivorie add 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

https://.app
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

0/140
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

0/280
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-group and iv-input-group-addon have role="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] and for/id.
  • Clicking an addon focuses the group's input, unless the click is on a button.
  • Icon-only iv-input-group-button needs aria-label.

Notes

  • Use ivInput and ivTextarea for standalone controls. Do not wrap them.
  • aria-invalid is not set for you. Bind it, for example [attr.aria-invalid]="control.invalid && control.touched".