Run the CLI
Use the CLI to add the component to your project.
npx zard-cli@latest add input-groupDisplay additional information or actions to an input or textarea.
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowUp, lucideCheck, lucideInfo, lucidePlus, lucideSearch } from '@ng-icons/lucide';
import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDividerComponent } from '@/shared/components/divider';
import { ZardDropdownImports } from '@/shared/components/dropdown';
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,
NgIcon,
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><ng-icon name="lucideSearch" /></ng-template>
<ng-template #check>
<div class="bg-primary size-4 rounded-full p-0.5">
<span class="flex items-center justify-center">
<ng-icon name="lucideCheck" class="text-primary-foreground size-3!" />
</span>
</div>
</ng-template>
<ng-template #info><ng-icon name="lucideInfo" 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" zSize="icon-sm">
<ng-icon name="lucidePlus" />
</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" zSize="icon-sm">
<ng-icon name="lucideArrowUp" />
</button>
</div>
</div>
</ng-template>
`,
viewProviders: [
provideIcons({
lucideSearch,
lucideCheck,
lucideInfo,
lucidePlus,
lucideArrowUp,
}),
],
})
export class ZardDemoInputGroupDefaultComponent {}
Use the CLI to add the component to your project.
npx zard-cli@latest add input-grouppnpm dlx zard-cli@latest add input-groupyarn dlx zard-cli@latest add input-groupbunx zard-cli@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(() => {
const isTextarea = this.contentInput()?.getType() === 'textarea';
return mergeClasses(
'w-full',
inputGroupVariants({
zSize: this.zSize(),
zDisabled: this.zDisabled() || this.zLoading(),
}),
!isTextarea && !this.zAddonBefore() ? 'pl-2.5' : '',
!isTextarea && !this.zAddonAfter() ? 'pr-2.5' : '',
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-lg flex items-stretch w-full min-w-0 transition-colors',
'border border-input dark:bg-input/30',
'[&_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-2.5! [&_textarea[z-input]]:py-2!',
'has-[textarea]:flex-col has-[textarea]:h-auto',
// focus state
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',
// disabled state
'has-disabled:bg-input/50 has-disabled:opacity-50 dark:has-disabled:bg-input/80',
// block align
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col',
),
{
variants: {
zSize: {
sm: 'h-8',
default: 'h-9',
lg: 'h-10',
},
zDisabled: {
true: 'cursor-not-allowed',
false: '',
},
},
defaultVariants: {
zSize: 'default',
zDisabled: false,
},
},
);
export const inputGroupAddonVariants = cva(
'items-center gap-1 py-1.5 cursor-text whitespace-nowrap font-medium text-muted-foreground select-none transition-colors disabled:pointer-events-none disabled:opacity-50 [&>svg:not([class*=size-])]:size-4',
{
variants: {
zType: {
default: 'justify-center',
textarea: 'justify-start w-full',
},
zSize: {
sm: 'text-sm',
default: 'text-sm',
lg: 'text-base',
},
zPosition: {
before: 'order-first',
after: 'order-last',
},
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',
zPosition: 'before',
class: 'pl-2 has-[>button]:ml-[-0.3rem]',
},
{
zType: 'default',
zPosition: 'after',
class: 'pr-2 has-[>button]:mr-[-0.3rem]',
},
{
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: 'w-full justify-start px-3 pt-2',
},
{
zType: 'textarea',
zPosition: 'after',
class: 'w-full justify-start px-3 pb-2',
},
],
},
);
export const inputGroupInputVariants = cva(
mergeClasses(
'font-normal flex has-[textarea]:h-auto w-full items-center rounded-lg bg-transparent 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:bg-transparent disabled:opacity-50 transition-colors',
'dark:bg-transparent dark:disabled:bg-transparent',
),
{
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 { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowUp, lucideCheck, lucideInfo, lucidePlus, lucideSearch } from '@ng-icons/lucide';
import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDividerComponent } from '@/shared/components/divider';
import { ZardDropdownImports } from '@/shared/components/dropdown';
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,
NgIcon,
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><ng-icon name="lucideSearch" /></ng-template>
<ng-template #check>
<div class="bg-primary size-4 rounded-full p-0.5">
<span class="flex items-center justify-center">
<ng-icon name="lucideCheck" class="text-primary-foreground size-3!" />
</span>
</div>
</ng-template>
<ng-template #info><ng-icon name="lucideInfo" 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" zSize="icon-sm">
<ng-icon name="lucidePlus" />
</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" zSize="icon-sm">
<ng-icon name="lucideArrowUp" />
</button>
</div>
</div>
</ng-template>
`,
viewProviders: [
provideIcons({
lucideSearch,
lucideCheck,
lucideInfo,
lucidePlus,
lucideArrowUp,
}),
],
})
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 { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideSearch } from '@ng-icons/lucide';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardInputGroupComponent } from '../input-group.component';
@Component({
selector: 'z-demo-input-group-loading',
imports: [ZardInputGroupComponent, ZardInputDirective, NgIcon],
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><ng-icon name="lucideSearch" /></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
viewProviders: [provideIcons({ lucideSearch })],
})
export class ZardDemoInputGroupLoadingComponent {}