Input Group

A flexible input group that combines inputs with addons, prefixes, and suffixes to improve usability.

PreviousNext
https://
.com
@
import { Component } from '@angular/core';
 
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
 
@Component({
  selector: 'z-demo-input-group-default',
  standalone: true,
  imports: [ZardInputGroupComponent, ZardInputDirective],
  template: `
    <div class="flex flex-col space-y-4">
      <z-input-group zAddOnBefore="https://" zAddOnAfter=".com" class="mb-4">
        <input z-input placeholder="example" />
      </z-input-group>
 
      <z-input-group zPrefix="$" zSuffix="USD" class="mb-4">
        <input z-input placeholder="0.00" type="number" />
      </z-input-group>
 
      <z-input-group zAddOnBefore="@">
        <input z-input placeholder="username" />
      </z-input-group>
    </div>
  `,
})
export class ZardDemoInputGroupDefaultComponent {}
 

Installation

1

Run the CLI

Use the CLI to add the component to your project.

npx @ngzard/ui add input-group
1

Add the component files

Create the component directory structure and add the following files to your project.

input-group.component.ts
input-group.component.ts
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input, type TemplateRef, ViewEncapsulation } from '@angular/core';
 
import type { ClassValue } from 'clsx';
 
import { inputGroupAddonVariants, inputGroupAffixVariants, inputGroupInputVariants, inputGroupVariants, type ZardInputGroupVariants } from './input-group.variants';
import { generateId, mergeClasses } from '../../shared/utils/utils';
import { ZardStringTemplateOutletDirective } from '../core/directives/string-template-outlet/string-template-outlet.directive';
 
@Component({
  selector: 'z-input-group',
  exportAs: 'zInputGroup',
  standalone: true,
  imports: [ZardStringTemplateOutletDirective],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  template: `
    <div
      [class]="wrapperClasses()"
      [attr.role]="'group'"
      [attr.aria-label]="zAriaLabel()"
      [attr.aria-labelledby]="zAriaLabelledBy()"
      [attr.aria-describedby]="zAriaDescribedBy()"
      [attr.aria-disabled]="zDisabled()"
      [attr.data-disabled]="zDisabled()"
    >
      @if (zAddOnBefore()) {
        <div [class]="addonBeforeClasses()" [id]="addonBeforeId()" [attr.aria-label]="zAddOnBeforeAriaLabel()" [attr.aria-disabled]="zDisabled()">
          <ng-container *zStringTemplateOutlet="zAddOnBefore()">{{ zAddOnBefore() }}</ng-container>
        </div>
      }
 
      <div [class]="inputWrapperClasses()">
        @if (zPrefix()) {
          <div [class]="prefixClasses()" [id]="prefixId()" [attr.aria-label]="zPrefixAriaLabel()" [attr.aria-hidden]="true">
            <ng-container *zStringTemplateOutlet="zPrefix()">{{ zPrefix() }}</ng-container>
          </div>
        }
 
        <ng-content select="input[z-input], textarea[z-input]"></ng-content>
 
        @if (zSuffix()) {
          <div [class]="suffixClasses()" [id]="suffixId()" [attr.aria-label]="zSuffixAriaLabel()" [attr.aria-hidden]="true">
            <ng-container *zStringTemplateOutlet="zSuffix()">{{ zSuffix() }}</ng-container>
          </div>
        }
      </div>
 
      @if (zAddOnAfter()) {
        <div [class]="addonAfterClasses()" [id]="addonAfterId()" [attr.aria-label]="zAddOnAfterAriaLabel()" [attr.aria-disabled]="zDisabled()">
          <ng-container *zStringTemplateOutlet="zAddOnAfter()">{{ zAddOnAfter() }}</ng-container>
        </div>
      }
    </div>
  `,
  host: {
    '[class]': 'classes()',
  },
})
export class ZardInputGroupComponent {
  readonly zSize = input<ZardInputGroupVariants['zSize']>('default');
  readonly zAddOnBefore = input<string | TemplateRef<void>>();
  readonly zAddOnAfter = input<string | TemplateRef<void>>();
  readonly zPrefix = input<string | TemplateRef<void>>();
  readonly zSuffix = input<string | TemplateRef<void>>();
  readonly zDisabled = input(false, { transform: booleanAttribute });
  readonly zBorderless = input(false, { transform: booleanAttribute });
  readonly zAriaLabel = input<string>();
  readonly zAriaLabelledBy = input<string>();
  readonly zAriaDescribedBy = input<string>();
  readonly zAddOnBeforeAriaLabel = input<string>();
  readonly zAddOnAfterAriaLabel = input<string>();
  readonly zPrefixAriaLabel = input<string>();
  readonly zSuffixAriaLabel = input<string>();
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses('w-full', this.class()));
 
  private readonly uniqueId = generateId('input-group');
  protected readonly addonBeforeId = computed(() => `${this.uniqueId}-addon-before`);
  protected readonly addonAfterId = computed(() => `${this.uniqueId}-addon-after`);
  protected readonly prefixId = computed(() => `${this.uniqueId}-prefix`);
  protected readonly suffixId = computed(() => `${this.uniqueId}-suffix`);
 
  protected readonly wrapperClasses = computed(() =>
    inputGroupVariants({
      zSize: this.zSize(),
      zDisabled: this.zDisabled(),
    }),
  );
 
  protected readonly addonBeforeClasses = computed(() =>
    inputGroupAddonVariants({
      zSize: this.zSize(),
      zPosition: 'before',
      zDisabled: this.zDisabled(),
      zBorderless: this.zBorderless(),
    }),
  );
 
  protected readonly addonAfterClasses = computed(() =>
    inputGroupAddonVariants({
      zSize: this.zSize(),
      zPosition: 'after',
      zDisabled: this.zDisabled(),
      zBorderless: this.zBorderless(),
    }),
  );
 
  protected readonly prefixClasses = computed(() =>
    inputGroupAffixVariants({
      zSize: this.zSize(),
      zPosition: 'prefix',
    }),
  );
 
  protected readonly suffixClasses = computed(() =>
    inputGroupAffixVariants({
      zSize: this.zSize(),
      zPosition: 'suffix',
    }),
  );
 
  protected readonly inputWrapperClasses = computed(() => {
    return mergeClasses(
      inputGroupInputVariants({
        zSize: this.zSize(),
        zHasPrefix: Boolean(this.zPrefix()),
        zHasSuffix: Boolean(this.zSuffix()),
        zHasAddonBefore: Boolean(this.zAddOnBefore()),
        zHasAddonAfter: Boolean(this.zAddOnAfter()),
        zDisabled: this.zDisabled(),
        zBorderless: this.zBorderless(),
      }),
      'relative',
    );
  });
}
 
input-group.variants.ts
input-group.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
 
export const inputGroupVariants = cva(
  'flex items-stretch w-full [&_input[z-input]]:!border-0 [&_input[z-input]]:!bg-transparent [&_input[z-input]]:!outline-none [&_input[z-input]]:!ring-0 [&_input[z-input]]:!ring-offset-0 [&_input[z-input]]:!px-0 [&_input[z-input]]:!py-0 [&_input[z-input]]:!h-full [&_input[z-input]]:flex-1 [&_textarea[z-input]]:!border-0 [&_textarea[z-input]]:!bg-transparent [&_textarea[z-input]]:!outline-none [&_textarea[z-input]]:!ring-0 [&_textarea[z-input]]:!ring-offset-0 [&_textarea[z-input]]:!px-0 [&_textarea[z-input]]:!py-0',
  {
    variants: {
      zSize: {
        sm: 'h-9',
        default: 'h-10',
        lg: 'h-11',
      },
      zDisabled: {
        true: 'opacity-50 cursor-not-allowed',
        false: '',
      },
    },
    defaultVariants: {
      zSize: 'default',
      zDisabled: false,
    },
  },
);
 
export const inputGroupAddonVariants = cva(
  'addon inline-flex items-center justify-center whitespace-nowrap text-sm font-medium border border-input bg-muted text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      zSize: {
        sm: 'h-9 px-3 text-xs',
        default: 'h-10 px-3 text-sm',
        lg: 'h-11 px-4 text-base',
      },
      zPosition: {
        before: 'rounded-l-md border-r-0',
        after: 'rounded-r-md border-l-0',
      },
      zDisabled: {
        true: 'cursor-not-allowed opacity-50 pointer-events-none',
        false: '',
      },
      zBorderless: {
        true: 'border-0 shadow-none',
        false: '',
      },
    },
    defaultVariants: {
      zSize: 'default',
      zPosition: 'before',
      zDisabled: false,
      zBorderless: false,
    },
  },
);
 
export const inputGroupAffixVariants = cva('absolute inset-y-0 flex items-center text-muted-foreground pointer-events-none z-10', {
  variants: {
    zSize: {
      sm: 'text-xs',
      default: 'text-sm',
      lg: 'text-base',
    },
    zPosition: {
      prefix: 'left-0 pl-3',
      suffix: 'right-0 pr-3',
    },
  },
  defaultVariants: {
    zSize: 'default',
    zPosition: 'prefix',
  },
});
 
export const inputGroupInputVariants = cva(
  'input-wrapper flex h-full w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
  {
    variants: {
      zSize: {
        sm: 'h-9 px-3 py-1 text-sm',
        default: 'h-10 px-3 py-2 text-sm',
        lg: 'h-11 px-4 py-2 text-base',
      },
      zHasPrefix: {
        true: '',
        false: '',
      },
      zHasSuffix: {
        true: '',
        false: '',
      },
      zHasAddonBefore: {
        true: 'border-l-0 rounded-l-none',
        false: '',
      },
      zHasAddonAfter: {
        true: 'border-r-0 rounded-r-none',
        false: '',
      },
      zDisabled: {
        true: 'cursor-not-allowed opacity-50',
        false: '',
      },
      zBorderless: {
        true: 'border-0 bg-transparent shadow-none',
        false: '',
      },
    },
    compoundVariants: [
      {
        zHasPrefix: true,
        zSize: 'sm',
        class: 'pl-7',
      },
      {
        zHasPrefix: true,
        zSize: 'default',
        class: 'pl-8',
      },
      {
        zHasPrefix: true,
        zSize: 'lg',
        class: 'pl-9',
      },
      {
        zHasSuffix: true,
        zSize: 'sm',
        class: 'pr-12',
      },
      {
        zHasSuffix: true,
        zSize: 'default',
        class: 'pr-14',
      },
      {
        zHasSuffix: true,
        zSize: 'lg',
        class: 'pr-16',
      },
    ],
    defaultVariants: {
      zSize: 'default',
      zHasPrefix: false,
      zHasSuffix: false,
      zHasAddonBefore: false,
      zHasAddonAfter: false,
      zDisabled: false,
      zBorderless: false,
    },
  },
);
 
export type ZardInputGroupVariants = VariantProps<typeof inputGroupVariants>;
 
index.ts
index.ts
export { ZardInputGroupComponent } from './input-group.component';
export * from './input-group.variants';
 

Examples

default

https://
.com
@
import { Component } from '@angular/core';
 
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
 
@Component({
  selector: 'z-demo-input-group-default',
  standalone: true,
  imports: [ZardInputGroupComponent, ZardInputDirective],
  template: `
    <div class="flex flex-col space-y-4">
      <z-input-group zAddOnBefore="https://" zAddOnAfter=".com" class="mb-4">
        <input z-input placeholder="example" />
      </z-input-group>
 
      <z-input-group zPrefix="$" zSuffix="USD" class="mb-4">
        <input z-input placeholder="0.00" type="number" />
      </z-input-group>
 
      <z-input-group zAddOnBefore="@">
        <input z-input placeholder="username" />
      </z-input-group>
    </div>
  `,
})
export class ZardDemoInputGroupDefaultComponent {}
 

size

@
.com
@
.com
@
.com
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
 
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
 
@Component({
  selector: 'z-demo-input-group-size',
  standalone: true,
  imports: [ZardInputGroupComponent, ZardInputDirective, FormsModule],
  template: `
    <div class="flex flex-col space-y-4">
      <z-input-group zSize="sm" zAddOnBefore="@" zAddOnAfter=".com" class="mb-4">
        <input z-input placeholder="Small" [(ngModel)]="smallValue" />
      </z-input-group>
 
      <z-input-group zSize="default" zAddOnBefore="@" zAddOnAfter=".com" class="mb-4">
        <input z-input placeholder="Default" [(ngModel)]="defaultValue" />
      </z-input-group>
 
      <z-input-group zSize="lg" zAddOnBefore="@" zAddOnAfter=".com">
        <input z-input placeholder="Large" [(ngModel)]="largeValue" />
      </z-input-group>
    </div>
  `,
})
export class ZardDemoInputGroupSizeComponent {
  smallValue = '';
  defaultValue = '';
  largeValue = '';
}
 

borderless

https://
.com
@
import { Component } from '@angular/core';
 
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
 
@Component({
  selector: 'z-demo-input-group-borderless',
  standalone: true,
  imports: [ZardInputGroupComponent, ZardInputDirective],
  template: `
    <div class="flex flex-col space-y-4">
      <z-input-group zPrefix="$" zSuffix="USD" zBorderless>
        <input z-input placeholder="0.00" type="number" />
      </z-input-group>
 
      <z-input-group zAddOnBefore="https://" zAddOnAfter=".com" zBorderless>
        <input z-input placeholder="example" />
      </z-input-group>
 
      <z-input-group zAddOnBefore="@" zBorderless>
        <input z-input placeholder="username" />
      </z-input-group>
    </div>
  `,
})
export class ZardDemoInputGroupBorderlessComponent {}
 

Input Group API Reference

Components

z-input-group

Input Type Default Description
zSize 'sm' | 'default' | 'lg' 'default' Size of the input group and all its elements
zDisabled boolean false Disable the entire input group
zBorderless boolean false Remove borders and background for a clean look
zAddOnBefore string | TemplateRef<void> undefined Content to display before the input
zAddOnAfter string | TemplateRef<void> undefined Content to display after the input
zPrefix string | TemplateRef<void> undefined Prefix content inside the input (left)
zSuffix string | TemplateRef<void> undefined Suffix content inside the input (right)
zAriaLabel string undefined Accessibility label for the input group
zAriaLabelledBy string undefined ID of element that labels the input group
zAriaDescribedBy string undefined ID of element that describes the input group
zAddOnBeforeAriaLabel string undefined Accessibility label for the before addon
zAddOnAfterAriaLabel string undefined Accessibility label for the after addon
zPrefixAriaLabel string undefined Accessibility label for the prefix
zSuffixAriaLabel string undefined Accessibility label for the suffix
class ClassValue '' Additional CSS classes