Run the CLI
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add input-groupA flexible input group that combines inputs with addons, prefixes, and suffixes to improve usability.
import { Component } from '@angular/core';
import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDividerComponent } from '@/shared/components/divider';
import { ZardDropdownImports } from '@/shared/components/dropdown';
import { ZardIconComponent } from '@/shared/components/icon';
import { ZardInputDirective } from '@/shared/components/input/input.directive';
import { ZardInputGroupComponent } from '@/shared/components/input-group/input-group.component';
import { ZardTooltipDirective } from '@/shared/components/tooltip';
@Component({
selector: 'z-demo-input-group-default',
imports: [
ZardButtonComponent,
ZardDropdownImports,
ZardIconComponent,
ZardInputDirective,
ZardInputGroupComponent,
ZardDividerComponent,
ZardTooltipDirective,
],
template: `
<div class="flex w-95 flex-col space-y-4">
<z-input-group [zAddonBefore]="search" zAddonAfter="12 results" class="mb-4">
<input z-input placeholder="Search..." />
</z-input-group>
<z-input-group zAddonBefore="https://" [zAddonAfter]="info" class="mb-4">
<input z-input placeholder="example.com" />
</z-input-group>
<z-input-group class="mb-4" [zAddonAfter]="areaAfter">
<textarea class="h-30 resize-none" z-input placeholder="Ask, Search or Chat..."></textarea>
</z-input-group>
<z-input-group [zAddonAfter]="check">
<input z-input placeholder="@zardui" />
</z-input-group>
</div>
<ng-template #search><z-icon zType="search" /></ng-template>
<ng-template #check>
<div class="bg-primary rounded-full p-0.5">
<z-icon zType="check" class="stroke-primary-foreground" zSize="sm" />
</div>
</ng-template>
<ng-template #info><z-icon zType="info" zTooltip="Element with tooltip" /></ng-template>
<ng-template #areaAfter>
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-1">
<button type="button" z-button zType="outline" zShape="circle" class="data-icon-only:size-6!">
<z-icon zType="plus" />
</button>
<button type="button" z-button zType="ghost" class="h-6" z-dropdown [zDropdownMenu]="menu">Auto</button>
<z-dropdown-menu-content #menu="zDropdownMenuContent" class="w-10">
<z-dropdown-menu-item>Auto</z-dropdown-menu-item>
<z-dropdown-menu-item>Agent</z-dropdown-menu-item>
<z-dropdown-menu-item>Manual</z-dropdown-menu-item>
</z-dropdown-menu-content>
</div>
<div class="flex h-auto items-center gap-0">
<span>52% used</span>
<z-divider zOrientation="vertical" class="h-4" />
<button type="button" z-button zType="outline" zShape="circle" class="data-icon-only:size-6!">
<z-icon zType="arrow-up" />
</button>
</div>
</div>
</ng-template>
`,
})
export class ZardDemoInputGroupDefaultComponent {}
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add input-grouppnpm dlx @ngzard/ui@latest add input-groupyarn dlx @ngzard/ui@latest add input-groupbunx @ngzard/ui@latest add input-groupCreate the component directory structure and add the following files to your project.
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
contentChild,
effect,
input,
type TemplateRef,
viewChild,
ViewEncapsulation,
} from '@angular/core';
import type { ClassValue } from 'clsx';
import { ZardIdDirective } from '@/shared/core';
import {
isTemplateRef,
ZardStringTemplateOutletDirective,
} from '@/shared/core/directives/string-template-outlet/string-template-outlet.directive';
import { mergeClasses } from '@/shared/utils/merge-classes';
import {
inputGroupAddonVariants,
inputGroupInputVariants,
inputGroupVariants,
type ZardInputGroupAddonAlignVariants,
type ZardInputGroupAddonPositionVariants,
} from './input-group.variants';
import { ZardInputDirective } from '../input/input.directive';
import type { ZardInputSizeVariants } from '../input/input.variants';
import { ZardLoaderComponent } from '../loader/loader.component';
@Component({
selector: 'z-input-group',
imports: [ZardStringTemplateOutletDirective, ZardLoaderComponent, ZardIdDirective],
template: `
<ng-container zardId="input-group" #z="zardId">
@let addonBefore = zAddonBefore();
@if (addonBefore) {
<div [class]="addonBeforeClasses()" [id]="addonBeforeId()" [attr.aria-disabled]="zDisabled() || zLoading()">
<ng-container *zStringTemplateOutlet="addonBefore">{{ addonBefore }}</ng-container>
</div>
}
<div [class]="inputWrapperClasses()">
<ng-content select="input[z-input], textarea[z-input]" />
@if (zLoading()) {
<z-loader zSize="sm" />
}
</div>
@let addonAfter = zAddonAfter();
@if (addonAfter) {
<div [class]="addonAfterClasses()" [id]="addonAfterId()" [attr.aria-disabled]="zDisabled() || zLoading()">
<ng-container *zStringTemplateOutlet="addonAfter">{{ addonAfter }}</ng-container>
</div>
}
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class]': 'classes()',
'[attr.aria-disabled]': 'zDisabled() || zLoading()',
'[attr.data-disabled]': 'zDisabled() || zLoading()',
'[attr.aria-busy]': 'zLoading()',
'data-slot': 'input-group',
role: 'group',
},
})
export class ZardInputGroupComponent {
readonly class = input<ClassValue>('');
readonly zAddonAfter = input<string | TemplateRef<void>>('');
readonly zAddonAlign = input<ZardInputGroupAddonAlignVariants>('inline');
readonly zAddonBefore = input<string | TemplateRef<void>>('');
readonly zDisabled = input(false, { transform: booleanAttribute });
readonly zLoading = input(false, { transform: booleanAttribute });
readonly zSize = input<ZardInputSizeVariants>('default');
private readonly contentInput = contentChild<ZardInputDirective>(ZardInputDirective);
private readonly uniqueId = viewChild<ZardIdDirective>('z');
protected readonly addonBeforeId = computed(() => `${this.uniqueId()?.id() ?? 'input-group'}-addon-before`);
protected readonly addonAfterId = computed(() => `${this.uniqueId()?.id() ?? 'input-group'}-addon-after`);
protected readonly isAddonBeforeTemplate = computed(() => isTemplateRef(this.zAddonBefore()));
protected readonly classes = computed(() =>
mergeClasses(
'w-full',
inputGroupVariants({
zSize: this.zSize(),
zDisabled: this.zDisabled() || this.zLoading(),
}),
this.class(),
),
);
protected readonly inputWrapperClasses = computed(() =>
mergeClasses(
inputGroupInputVariants({
zSize: this.zSize(),
zHasAddonBefore: Boolean(this.zAddonBefore()),
zHasAddonAfter: Boolean(this.zAddonAfter()),
zDisabled: this.zDisabled() || this.zLoading(),
}),
'relative',
),
);
protected readonly addonAfterClasses = computed(() => this.addonClasses('after'));
protected readonly addonBeforeClasses = computed(() =>
mergeClasses(this.addonClasses('before'), this.isAddonBeforeTemplate() ? 'pr-0.5' : ''),
);
constructor() {
effect(() => {
const contentInput = this.contentInput();
const disabled = this.zDisabled();
const size = this.zSize();
if (size) {
contentInput?.size.set(size);
}
contentInput?.disable(disabled);
contentInput?.setDataSlot('input-group-control');
});
}
private addonClasses(position: ZardInputGroupAddonPositionVariants): string {
return mergeClasses(
inputGroupAddonVariants({
zAlign: this.zAddonAlign(),
zDisabled: this.zDisabled() || this.zLoading(),
zPosition: position,
zSize: this.zSize(),
zType: this.contentInput()?.getType() ?? 'default',
}),
);
}
}
import { cva, type VariantProps } from 'class-variance-authority';
import { mergeClasses } from '@/shared/utils/merge-classes';
export const inputGroupVariants = cva(
mergeClasses(
'rounded-md flex px-3 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!',
'min-w-0 has-[textarea]:flex-col has-[textarea]:p-3 has-[textarea]:h-auto border border-input',
// focus state
'has-[[data-slot=input-group-control]:focus-visible]:border has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
),
{
variants: {
zSize: {
sm: 'h-8',
default: 'h-9',
lg: 'h-10',
},
zDisabled: {
true: 'opacity-50 cursor-not-allowed',
false: '',
},
},
defaultVariants: {
zSize: 'default',
zDisabled: false,
},
},
);
export const inputGroupAddonVariants = cva(
'items-center whitespace-nowrap font-medium text-muted-foreground transition-colors disabled:pointer-events-none disabled:opacity-50',
{
variants: {
zType: {
default: 'justify-center',
textarea: 'justify-start w-full',
},
zSize: {
sm: 'text-xs',
default: 'text-sm',
lg: '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: '',
},
zAlign: {
block: 'flex',
inline: 'inline-flex',
},
},
defaultVariants: {
zAlign: 'inline',
zPosition: 'before',
zDisabled: false,
zSize: 'default',
},
compoundVariants: [
{
zType: 'default',
zSize: 'default',
class: 'h-8.5',
},
{
zType: 'default',
zSize: 'sm',
class: 'h-7.5',
},
{
zType: 'default',
zSize: 'lg',
class: 'h-9.5',
},
{
zType: 'textarea',
zPosition: 'before',
class: 'mb-2',
},
{
zType: 'textarea',
zPosition: 'after',
class: 'mt-2',
},
],
},
);
export const inputGroupInputVariants = cva(
mergeClasses(
'font-normal flex has-[textarea]:h-auto w-full items-center rounded-md bg-background ring-offset-background',
'file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground',
'focus-within:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
),
{
variants: {
zSize: {
sm: 'h-7.5 px-0.5 py-0 text-xs',
default: 'h-8.5 px-0.5 py-0 text-sm',
lg: 'h-9.5 px-0.5 py-0 text-base',
},
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: '',
},
},
defaultVariants: {
zSize: 'default',
zHasAddonBefore: false,
zHasAddonAfter: false,
zDisabled: false,
},
},
);
export type ZardInputGroupAddonAlignVariants = NonNullable<VariantProps<typeof inputGroupAddonVariants>['zAlign']>;
export type ZardInputGroupAddonPositionVariants = NonNullable<
VariantProps<typeof inputGroupAddonVariants>['zPosition']
>;
export * from './input-group.component';
export * from './input-group.variants';
import { Component } from '@angular/core';
import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDividerComponent } from '@/shared/components/divider';
import { ZardDropdownImports } from '@/shared/components/dropdown';
import { ZardIconComponent } from '@/shared/components/icon';
import { ZardInputDirective } from '@/shared/components/input/input.directive';
import { ZardInputGroupComponent } from '@/shared/components/input-group/input-group.component';
import { ZardTooltipDirective } from '@/shared/components/tooltip';
@Component({
selector: 'z-demo-input-group-default',
imports: [
ZardButtonComponent,
ZardDropdownImports,
ZardIconComponent,
ZardInputDirective,
ZardInputGroupComponent,
ZardDividerComponent,
ZardTooltipDirective,
],
template: `
<div class="flex w-95 flex-col space-y-4">
<z-input-group [zAddonBefore]="search" zAddonAfter="12 results" class="mb-4">
<input z-input placeholder="Search..." />
</z-input-group>
<z-input-group zAddonBefore="https://" [zAddonAfter]="info" class="mb-4">
<input z-input placeholder="example.com" />
</z-input-group>
<z-input-group class="mb-4" [zAddonAfter]="areaAfter">
<textarea class="h-30 resize-none" z-input placeholder="Ask, Search or Chat..."></textarea>
</z-input-group>
<z-input-group [zAddonAfter]="check">
<input z-input placeholder="@zardui" />
</z-input-group>
</div>
<ng-template #search><z-icon zType="search" /></ng-template>
<ng-template #check>
<div class="bg-primary rounded-full p-0.5">
<z-icon zType="check" class="stroke-primary-foreground" zSize="sm" />
</div>
</ng-template>
<ng-template #info><z-icon zType="info" zTooltip="Element with tooltip" /></ng-template>
<ng-template #areaAfter>
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-1">
<button type="button" z-button zType="outline" zShape="circle" class="data-icon-only:size-6!">
<z-icon zType="plus" />
</button>
<button type="button" z-button zType="ghost" class="h-6" z-dropdown [zDropdownMenu]="menu">Auto</button>
<z-dropdown-menu-content #menu="zDropdownMenuContent" class="w-10">
<z-dropdown-menu-item>Auto</z-dropdown-menu-item>
<z-dropdown-menu-item>Agent</z-dropdown-menu-item>
<z-dropdown-menu-item>Manual</z-dropdown-menu-item>
</z-dropdown-menu-content>
</div>
<div class="flex h-auto items-center gap-0">
<span>52% used</span>
<z-divider zOrientation="vertical" class="h-4" />
<button type="button" z-button zType="outline" zShape="circle" class="data-icon-only:size-6!">
<z-icon zType="arrow-up" />
</button>
</div>
</div>
</ng-template>
`,
})
export class ZardDemoInputGroupDefaultComponent {}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
@Component({
selector: 'z-demo-input-group-text',
imports: [ZardInputGroupComponent, ZardInputDirective, ZardButtonComponent],
template: `
<z-input-group zAddonBefore="$" zAddonAfter="USD" class="mb-4">
<input z-input placeholder="0.00" type="number" />
</z-input-group>
<z-input-group zAddonBefore="https://" zAddonAfter=".com" class="mb-4">
<input z-input placeholder="example.com" />
</z-input-group>
<z-input-group zAddonAfter="@company.com" class="mb-4">
<input z-input placeholder="Enter your username" />
</z-input-group>
<z-input-group [zAddonAfter]="actions" class="mb-4">
<textarea z-input class="resize-none" placeholder="Enter your message"></textarea>
</z-input-group>
<ng-template #actions>
<div class="flex w-full items-center justify-between">
<span class="text-muted-foreground text-xs">120 characters left</span>
<button type="submit" z-button zSize="sm">Submit</button>
</div>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoInputGroupTextComponent {}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
@Component({
selector: 'z-demo-input-group-size',
imports: [ZardInputGroupComponent, ZardInputDirective],
template: `
<div class="flex flex-col space-y-4">
<z-input-group zSize="sm" zAddonBefore="https://" zAddonAfter=".com" class="mb-4">
<input z-input placeholder="Small" [(value)]="smallValue" />
</z-input-group>
<z-input-group zSize="default" zAddonBefore="https://" zAddonAfter=".com" class="mb-4">
<input z-input placeholder="Default" [(value)]="defaultValue" />
</z-input-group>
<z-input-group zSize="lg" zAddonBefore="https://" zAddonAfter=".com">
<input z-input placeholder="Large" [(value)]="largeValue" />
</z-input-group>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoInputGroupSizeComponent {
smallValue = '';
defaultValue = '';
largeValue = '';
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
@Component({
selector: 'z-demo-input-group-borderless',
imports: [ZardInputGroupComponent, ZardInputDirective],
template: `
<div class="flex flex-col space-y-4">
<z-input-group zAddonBefore="$" zAddonAfter="USD" class="border-0">
<input z-input placeholder="0.00" type="number" />
</z-input-group>
<z-input-group zAddonBefore="https://" zAddonAfter=".com" class="border-0">
<input z-input placeholder="example" />
</z-input-group>
<z-input-group zAddonBefore="@" class="border-0">
<input z-input placeholder="username" />
</z-input-group>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoInputGroupBorderlessComponent {}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardIconComponent } from '../../icon/icon.component';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
@Component({
selector: 'z-demo-input-group-loading',
imports: [ZardInputGroupComponent, ZardInputDirective, ZardIconComponent],
template: `
<div class="flex flex-col space-y-4">
<z-input-group [zAddonBefore]="search" zLoading>
<input z-input type="text" placeholder="Search..." />
</z-input-group>
</div>
<ng-template #search><z-icon zType="search" /></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoInputGroupLoadingComponent {}