Input Group
Group related input controls with prefix and suffix elements for enhanced form fields. Apply av-input-group to a container <div>.
Import
import { AvInputGroupImports } from '@avesra/angular'; Compose with av-label, av-description, and av-field-error. For form-level validation and submission, see Form.
Usage
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-basic-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-email">Email address</label>
<div av-input-group>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-email"
type="email"
placeholder="name@email.com"
/>
</div>`,
})
export class InputGroupBasicDemo {}Anatomy
<label av-label for="field">Label</label>
<div av-input-group>
<div av-input-group-prefix><!-- icon or text --></div>
<input av-input-group-input id="field" type="text" />
<!-- Or use textarea[av-input-group-textarea] for multiline -->
<div av-input-group-suffix><!-- icon, text, or actions --></div>
</div>
<p av-description>Helper text</p>
<p av-field-error [visible]="false">Error message</p>Input Group wraps an input field with optional prefix and suffix elements, creating a visually cohesive group. Add icons, text, buttons, or other elements before or after the control. Use av-input-group-input on a native <input> for single-line fields, or av-input-group-textarea on a native <textarea> for multiline text.
Variants
The Input Group component supports two visual variants:
primary(default) — Standard styling with shadow, suitable for most use casessecondary— Lower emphasis variant without shadow, suitable for use in Surface components
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-variants-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-4' },
template: `<div class="flex flex-col gap-1">
<label av-label for="ig-primary">Primary variant</label>
<div av-input-group variant="primary">
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-primary"
placeholder="name@email.com"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label av-label for="ig-secondary">Secondary variant</label>
<div av-input-group variant="secondary">
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-secondary"
placeholder="name@email.com"
/>
</div>
</div>`,
})
export class InputGroupVariantsDemo {}In Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
We'll never share this with anyone else
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputGroupImports,
AvLabelComponent,
AvSurfaceComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-on-surface-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvDescriptionComponent,
AvSurfaceComponent,
AppIconComponent,
],
host: { class: 'flex w-full items-center justify-center' },
template: `<div av-surface class="rounded-2xl p-6">
<div class="flex w-full max-w-72 flex-col gap-1">
<label av-label for="ig-on-surface">Email address</label>
<div av-input-group variant="secondary">
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-on-surface"
type="email"
placeholder="name@email.com"
/>
</div>
<p av-description>We'll never share this with anyone else</p>
</div>
</div>`,
})
export class InputGroupOnSurfaceDemo {}Loading State
Show a loading spinner in the suffix to indicate processing.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvSpinnerComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-loading-suffix-demo',
imports: [
AvInputGroupImports,
AvSpinnerComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<div av-input-group>
<input av-input-group-input value="Sending..." aria-label="Status" />
<div av-input-group-suffix>
<span av-spinner size="sm" color="current" aria-label="Loading"></span>
</div>
</div>`,
})
export class InputGroupWithLoadingSuffixDemo {}Required Field
Mark the field as required with required on the label and the native control. Pair with av-description for helper text.
What customers would pay
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-required-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvDescriptionComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-4' },
template: `<div class="flex flex-col gap-1">
<label av-label for="ig-required-email" required>Email address</label>
<div av-input-group>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-required-email"
type="email"
placeholder="name@email.com"
required
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label av-label for="ig-required-price" required>Set a price</label>
<div av-input-group>
<div av-input-group-prefix>$</div>
<input
av-input-group-input
id="ig-required-price"
type="number"
placeholder="0"
required
/>
<div av-input-group-suffix>USD</div>
</div>
<p av-description>What customers would pay</p>
</div>`,
})
export class InputGroupRequiredDemo {}Disabled State
Set disabled on the group to disable the control and style all parts together. The inner input inherits disabled state from context.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-disabled-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-4' },
template: `<div class="flex flex-col gap-1">
<label av-label for="ig-disabled-email" disabled>Email address</label>
<div av-input-group disabled>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-disabled-email"
type="email"
value="name@email.com"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label av-label for="ig-disabled-price" disabled>Set a price</label>
<div av-input-group disabled>
<div av-input-group-prefix>$</div>
<input av-input-group-input id="ig-disabled-price" type="number" value="10" />
<div av-input-group-suffix>USD</div>
</div>
</div>`,
})
export class InputGroupDisabledDemo {}Full Width
Set full-width on the group to expand to the container width.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-full-width-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-sm flex-col gap-4' },
template: `<div class="flex w-full flex-col gap-1">
<label av-label for="ig-fw-email">Email address</label>
<div av-input-group full-width>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-fw-email"
type="email"
placeholder="name@email.com"
/>
</div>
</div>
<div class="flex w-full flex-col gap-1">
<label av-label for="ig-fw-password">Password</label>
<div av-input-group full-width>
<input
av-input-group-input
id="ig-fw-password"
type="password"
placeholder="Enter password"
/>
<div av-input-group-suffix>
<app-icon icon="solar:eye-linear" size="16" class="text-muted" />
</div>
</div>
</div>`,
})
export class InputGroupFullWidthDemo {}Text Prefix
Use text as a prefix, such as currency symbols or protocol prefixes.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-text-prefix-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-text-prefix">Website</label>
<div av-input-group>
<div av-input-group-prefix>https://</div>
<input av-input-group-input id="ig-text-prefix" value="avesraui.com" />
</div>`,
})
export class InputGroupWithTextPrefixDemo {}Text Suffix
Use text as a suffix, such as domain extensions or units.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-text-suffix-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-text-suffix">Website</label>
<div av-input-group>
<input av-input-group-input id="ig-text-suffix" value="avesraui" />
<div av-input-group-suffix>.com</div>
</div>`,
})
export class InputGroupWithTextSuffixDemo {}Icon Prefix and Text Suffix
Combine an icon prefix with a text suffix.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-with-icon-prefix-and-text-suffix-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-icon-text">Website</label>
<div av-input-group>
<div av-input-group-prefix>
<app-icon icon="solar:global-linear" size="16" class="text-muted" />
</div>
<input av-input-group-input id="ig-icon-text" value="avesraui" />
<div av-input-group-suffix>.com</div>
</div>`,
})
export class InputGroupWithIconPrefixAndTextSuffixDemo {}Password Toggle
Use a button in the suffix to toggle password visibility.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-password-with-toggle-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvButtonComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-password">Password</label>
<div av-input-group>
<input
av-input-group-input
id="ig-password"
[type]="passwordVisible() ? 'text' : 'password'"
[value]="passwordVisible() ? '87$2h.3diua' : '••••••••'"
readonly
/>
<div av-input-group-suffix class="pr-0">
<button
av-button
type="button"
size="sm"
variant="ghost"
icon-only
[attr.aria-label]="passwordVisible() ? 'Hide password' : 'Show password'"
(click)="togglePasswordVisibility()"
>
@if (passwordVisible()) {
<app-icon icon="solar:eye-linear" size="16" />
} @else {
<app-icon icon="solar:eye-closed-linear" size="16" />
}
</button>
</div>
</div>`,
})
export class InputGroupPasswordWithToggleDemo {
readonly passwordVisible = signal(false);
togglePasswordVisibility(): void {
this.passwordVisible.update((value) => !value);
}
}Keyboard Shortcut
Display keyboard shortcuts using the Kbd component.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvKbdImports,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-keyboard-shortcut-demo',
imports: [
AvInputGroupImports,
AvKbdImports,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<div av-input-group>
<input av-input-group-input placeholder="Command" aria-label="Command" />
<div av-input-group-suffix class="pr-2">
<kbd av-kbd>
<abbr av-kbd-abbr key-value="command"></abbr>
<span av-kbd-content>K</span>
</kbd>
</div>
</div>`,
})
export class InputGroupWithKeyboardShortcutDemo {}Badge Suffix
Add a badge or chip in the suffix to show status or labels.
import { Component } from '@angular/core';
import {
AvChipImports,
AvInputGroupImports,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-badge-suffix-demo',
imports: [
AvInputGroupImports,
AvChipImports,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<div av-input-group>
<input
av-input-group-input
placeholder="Email address"
aria-label="Email address"
/>
<div av-input-group-suffix class="pr-2">
<span av-chip color="accent" size="md" variant="soft" label="Pro"></span>
</div>
</div>`,
})
export class InputGroupWithBadgeSuffixDemo {}Validation
Set invalid on the group (and optionally the label) to reflect an invalid state. Show messages with av-field-error.
Please enter a valid email address
Price must be greater than 0
import { Component } from '@angular/core';
import {
AvFieldErrorComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-invalid-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvFieldErrorComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-4' },
template: `<div class="flex flex-col gap-1">
<label av-label for="ig-invalid-email" required invalid>Email address</label>
<div av-input-group invalid>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-invalid-email"
type="email"
placeholder="name@email.com"
required
/>
</div>
<p av-field-error [visible]="true">Please enter a valid email address</p>
</div>
<div class="flex flex-col gap-1">
<label av-label for="ig-invalid-price" required invalid>Set a price</label>
<div av-input-group invalid>
<div av-input-group-prefix>$</div>
<input
av-input-group-input
id="ig-invalid-price"
type="number"
placeholder="0"
required
/>
<div av-input-group-suffix>USD</div>
</div>
<p av-field-error [visible]="true">Price must be greater than 0</p>
</div>`,
})
export class InputGroupInvalidDemo {}With Prefix Icon
Add an icon before the input field.
We'll never share this with anyone else
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-with-prefix-icon-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvDescriptionComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-prefix-icon">Email address</label>
<div av-input-group>
<div av-input-group-prefix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
<input
av-input-group-input
id="ig-prefix-icon"
type="email"
placeholder="name@email.com"
/>
</div>
<p av-description>We'll never share this with anyone else</p>`,
})
export class InputGroupWithPrefixIconDemo {}With Suffix Icon
Add an icon after the input field.
We don't send spam
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-with-suffix-icon-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvDescriptionComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-suffix-icon">Email address</label>
<div av-input-group>
<input
av-input-group-input
id="ig-suffix-icon"
type="email"
placeholder="name@email.com"
/>
<div av-input-group-suffix>
<app-icon icon="solar:letter-linear" size="16" class="text-muted" />
</div>
</div>
<p av-description>We don't send spam</p>`,
})
export class InputGroupWithSuffixIconDemo {}With Prefix and Suffix
Combine both prefix and suffix elements.
What customers would pay
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-with-prefix-and-suffix-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
AvDescriptionComponent,
],
host: { class: 'flex w-full max-w-72 flex-col gap-1' },
template: `<label av-label for="ig-price">Set a price</label>
<div av-input-group>
<div av-input-group-prefix>$</div>
<input av-input-group-input id="ig-price" type="number" value="10" />
<div av-input-group-suffix>USD</div>
</div>
<p av-description>What customers would pay</p>`,
})
export class InputGroupWithPrefixAndSuffixDemo {}With TextArea
Use textarea[av-input-group-textarea] for multiline text inputs with prefix and suffix elements. When a textarea is present, the container automatically adjusts its height and aligns prefix/suffix elements to the top.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvInputGroupImports,
AvSpinnerComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-input-group-with-textarea-demo',
imports: [
AvInputGroupImports,
AvButtonComponent,
AvSpinnerComponent,
AppIconComponent,
],
host: { class: 'flex w-full max-w-lg flex-col' },
template: `<div
av-input-group
full-width
class="flex flex-col gap-2 rounded-3xl py-2"
aria-label="Prompt input"
>
<div av-input-group-prefix class="px-3 py-0">
<button av-button type="button" size="sm" variant="outline">
<app-icon icon="solar:mention-circle-linear" size="16" />
Add Context
</button>
</div>
<textarea
av-input-group-textarea
class="w-full resize-none px-3.5 py-0"
placeholder="Assign tasks or ask anything..."
rows="5"
[value]="promptValue()"
(input)="onPromptInput($event)"
></textarea>
<div av-input-group-suffix class="flex w-full items-center gap-1.5 px-3 py-0">
<button av-button type="button" size="sm" variant="tertiary" icon-only aria-label="Attach file">
<app-icon icon="solar:add-circle-linear" size="16" />
</button>
<div class="ml-auto flex items-center gap-1.5">
<button av-button type="button" size="sm" variant="ghost" icon-only aria-label="Voice input">
<app-icon icon="solar:microphone-linear" size="16" />
</button>
<button
av-button
type="button"
size="sm"
icon-only
aria-label="Send prompt"
[disabled]="!promptValue().trim() || isSubmitting()"
(click)="handleSubmit()"
>
@if (isSubmitting()) {
<span av-spinner size="sm" color="current"></span>
} @else {
<app-icon icon="solar:arrow-up-linear" size="16" />
}
</button>
</div>
</div>
</div>`,
})
export class InputGroupWithTextareaDemo {
readonly promptValue = signal('');
readonly isSubmitting = signal(false);
onPromptInput(event: Event): void {
this.promptValue.set((event.target as HTMLTextAreaElement).value);
}
handleSubmit(): void {
if (!this.promptValue().trim()) {
return;
}
this.isSubmitting.set(true);
setTimeout(() => {
this.isSubmitting.set(false);
this.promptValue.set('');
}, 1000);
}
}Styling
Passing Tailwind CSS classes
Pass utility classes on the group host or on prefix, input, and suffix parts.
import { Component } from '@angular/core';
import {
AvInputGroupImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-group-custom-styling-demo',
imports: [
AvInputGroupImports,
AvLabelComponent,
],
host: { class: 'flex w-full max-w-80 flex-col gap-1' },
template: `<label av-label for="ig-custom">Website</label>
<div av-input-group class="rounded-xl border-2 border-accent">
<div av-input-group-prefix class="bg-accent/10 text-accent">https://</div>
<input av-input-group-input id="ig-custom" class="font-medium" />
<div av-input-group-suffix class="bg-accent/10 text-accent">.com</div>
</div>`,
})
export class InputGroupCustomStylingDemo {}Customizing the component classes
Input Group uses BEM CSS classes that can be customized. Override the component classes with @layer components to match your design system.
@layer components {
.av-input-group {
@apply bg-field text-field-foreground shadow-field rounded-field inline-flex min-h-9 items-center overflow-hidden border text-sm outline-none;
}
.av-input-group__input {
@apply flex-1 rounded-none border-0 bg-transparent px-3 py-2 shadow-none outline-none;
}
.av-input-group__prefix {
@apply text-field-placeholder flex h-full items-center justify-center bg-transparent px-3;
}
.av-input-group__suffix {
@apply text-field-placeholder flex h-full items-center justify-center bg-transparent px-3;
}
.av-input-group--secondary {
@apply shadow-none;
background-color: var(--color-default);
}
}CSS Classes
.av-input-group— Root container with border, background, and flex layout. Usesmin-h-9for flexible height anditems-centerby default, switching toitems-startwhen a textarea is present..av-input-group__input— Input/textarea with transparent background and no border. Also used as the base class for textarea elements..av-input-group__prefix— Prefix container. Aligns to top when used with textarea..av-input-group__suffix— Suffix container. Aligns to top when used with textarea..av-input-group--primary— Primary variant with shadow (default).av-input-group--secondary— Secondary variant without shadow, suitable for use in surfaces.av-input-group--full-width— Full-width layout
When using av-input-group-textarea, the container switches from items-center to items-start and uses height: auto. Textarea-specific styles apply via the [data-slot="input-group-textarea"] attribute selector.
Interactive States
- Hover:
:hoveror[data-hovered="true"] - Focus Within: focus styles apply when the inner input/textarea is focused (
:focus-within/ field focus tokens) - Invalid:
[data-invalid="true"](also syncsaria-invalidon the control) - Disabled:
[data-disabled="true"]or[aria-disabled="true"]
API
Root props for div[av-input-group]. The input and textarea parts accept standard HTML attributes and implement ControlValueAccessor for [(ngModel)] or reactive forms. Prefix and suffix are content projection slots. Compose with av-label, av-description, and av-field-error for field chrome.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'primary' | Visual variant. `primary` is the default style with shadow. `secondary` is a lower emphasis variant without shadow, suitable for use in surfaces. |
full-width | boolean | false | Whether the input group should take full width of its container. |
disabled | boolean | false | Disables interaction for the group and its controls. Propagates to nested input/textarea via context. |
invalid | boolean | false | Marks the group as invalid. Propagates `aria-invalid` to nested input/textarea via context. |