Run the CLI
Use the CLI to add the component to your project.
npx @ngzard/ui add segmentedA set of two or more segments, each of which functions as a mutually exclusive button. Based on shadcn/ui's Tabs component pattern, providing a clean way to create toggle controls with single selection.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardSegmentedComponent } from '../segmented.component';
@Component({
selector: 'zard-demo-segmented-default',
standalone: true,
imports: [ZardSegmentedComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <z-segmented [zOptions]="options" zDefaultValue="all" (zChange)="onSelectionChange($event)"> </z-segmented> `,
})
export class ZardDemoSegmentedDefaultComponent {
options = [
{ value: 'all', label: 'All' },
{ value: 'unread', label: 'Unread' },
{ value: 'archived', label: 'Archived' },
];
onSelectionChange(value: string) {
console.log('Selected:', value);
}
}
Use the CLI to add the component to your project.
npx @ngzard/ui add segmentedpnpm dlx @ngzard/ui add segmentedyarn dlx @ngzard/ui add segmentedbunx @ngzard/ui add segmentedCreate the component directory structure and add the following files to your project.
import { ChangeDetectionStrategy, Component, computed, contentChildren, effect, forwardRef, input, type OnInit, output, signal, ViewEncapsulation } from '@angular/core';
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { mergeClasses } from '../../shared/utils/utils';
import { segmentedItemVariants, segmentedVariants, type ZardSegmentedVariants } from './segmented.variants';
import type { ClassValue } from 'clsx';
export interface SegmentedOption {
value: string;
label: string;
disabled?: boolean;
}
@Component({
selector: 'z-segmented-item',
standalone: true,
template: `<ng-content></ng-content>`,
encapsulation: ViewEncapsulation.None,
})
export class ZardSegmentedItemComponent {
readonly value = input.required<string>();
readonly label = input.required<string>();
readonly disabled = input(false);
}
@Component({
selector: 'z-segmented',
exportAs: 'zSegmented',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<div [class]="classes()" role="tablist" [attr.aria-label]="zAriaLabel()">
@if (zOptions().length > 0) {
@for (option of zOptions(); track option.value) {
<button
type="button"
role="tab"
[class]="getItemClasses(option.value)"
[disabled]="option.disabled || zDisabled()"
[attr.aria-selected]="isSelected(option.value)"
[attr.aria-controls]="option.value + '-panel'"
[attr.id]="option.value + '-tab'"
(click)="selectOption(option.value)"
>
{{ option.label }}
</button>
}
} @else {
@for (item of items(); track item.value()) {
<button
type="button"
role="tab"
[class]="getItemClasses(item.value())"
[disabled]="item.disabled() || zDisabled()"
[attr.aria-selected]="isSelected(item.value())"
[attr.aria-controls]="item.value() + '-panel'"
[attr.id]="item.value() + '-tab'"
(click)="selectOption(item.value())"
>
{{ item.label() }}
</button>
}
}
</div>
`,
host: {
'[class]': 'wrapperClasses()',
},
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ZardSegmentedComponent),
multi: true,
},
],
})
export class ZardSegmentedComponent implements ControlValueAccessor, OnInit {
private readonly itemComponents = contentChildren(ZardSegmentedItemComponent);
readonly class = input<ClassValue>('');
readonly zSize = input<ZardSegmentedVariants['zSize']>('default');
readonly zOptions = input<SegmentedOption[]>([]);
readonly zDefaultValue = input<string>('');
readonly zDisabled = input(false);
readonly zAriaLabel = input<string>('Segmented control');
readonly zChange = output<string>();
protected readonly selectedValue = signal<string>('');
protected readonly items = signal<readonly ZardSegmentedItemComponent[]>([]);
// eslint-disable-next-line @typescript-eslint/no-empty-function
private onChange: (value: string) => void = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
private onTouched = () => {};
constructor() {
effect(() => {
this.items.set(this.itemComponents());
});
}
ngOnInit() {
// Initialize with default value
if (this.zDefaultValue()) {
this.selectedValue.set(this.zDefaultValue());
}
}
protected readonly classes = computed(() => mergeClasses(segmentedVariants({ zSize: this.zSize() }), this.class()));
protected readonly wrapperClasses = computed(() => 'inline-block');
protected getItemClasses(value: string): string {
return segmentedItemVariants({
zSize: this.zSize(),
isActive: this.isSelected(value),
});
}
protected isSelected(value: string): boolean {
return this.selectedValue() === value;
}
protected selectOption(value: string) {
if (this.zDisabled()) return;
const option = this.zOptions().find(opt => opt.value === value);
const item = this.items().find(item => item.value() === value);
if (option?.disabled || item?.disabled()) return;
this.selectedValue.set(value);
this.onChange(value);
this.onTouched();
this.zChange.emit(value);
}
// ControlValueAccessor implementation
writeValue(value: string): void {
this.selectedValue.set(value ?? '');
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(_isDisabled: boolean): void {
// Handled by zDisabled input
}
}
import { cva, type VariantProps } from 'class-variance-authority';
export const segmentedVariants = cva('inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground', {
variants: {
zSize: {
sm: 'h-9 text-xs',
default: 'h-10 text-sm',
lg: 'h-12 text-base',
},
},
defaultVariants: {
zSize: 'default',
},
});
export const segmentedItemVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all 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: 'px-2 py-1 text-xs',
default: 'px-3 py-1.5 text-sm',
lg: 'px-4 py-2 text-base',
},
isActive: {
true: 'bg-background text-foreground shadow-sm',
false: 'hover:bg-muted/50',
},
},
defaultVariants: {
zSize: 'default',
isActive: false,
},
},
);
export type ZardSegmentedVariants = VariantProps<typeof segmentedVariants>;
export type ZardSegmentedItemVariants = VariantProps<typeof segmentedItemVariants>;
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardSegmentedComponent } from '../segmented.component';
@Component({
selector: 'zard-demo-segmented-default',
standalone: true,
imports: [ZardSegmentedComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <z-segmented [zOptions]="options" zDefaultValue="all" (zChange)="onSelectionChange($event)"> </z-segmented> `,
})
export class ZardDemoSegmentedDefaultComponent {
options = [
{ value: 'all', label: 'All' },
{ value: 'unread', label: 'Unread' },
{ value: 'archived', label: 'Archived' },
];
onSelectionChange(value: string) {
console.log('Selected:', value);
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardSegmentedComponent } from '../segmented.component';
@Component({
selector: 'zard-demo-segmented-sizes',
standalone: true,
imports: [ZardSegmentedComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Small</label>
<z-segmented zSize="sm" [zOptions]="options" zDefaultValue="tab1"> </z-segmented>
</div>
<div>
<label class="block text-sm font-medium mb-2">Default</label>
<z-segmented [zOptions]="options" zDefaultValue="tab1"> </z-segmented>
</div>
<div>
<label class="block text-sm font-medium mb-2">Large</label>
<z-segmented zSize="lg" [zOptions]="options" zDefaultValue="tab1"> </z-segmented>
</div>
</div>
`,
})
export class ZardDemoSegmentedSizesComponent {
options = [
{ value: 'tab1', label: 'Tab 1' },
{ value: 'tab2', label: 'Tab 2' },
{ value: 'tab3', label: 'Tab 3' },
];
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardSegmentedComponent } from '../segmented.component';
@Component({
selector: 'zard-demo-segmented-disabled',
standalone: true,
imports: [ZardSegmentedComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">With disabled options</label>
<z-segmented [zOptions]="optionsWithDisabled" zDefaultValue="enabled1"> </z-segmented>
</div>
<div>
<label class="block text-sm font-medium mb-2">Entire component disabled</label>
<z-segmented [zOptions]="options" zDefaultValue="tab1" [zDisabled]="true"> </z-segmented>
</div>
</div>
`,
})
export class ZardDemoSegmentedDisabledComponent {
options = [
{ value: 'tab1', label: 'Tab 1' },
{ value: 'tab2', label: 'Tab 2' },
{ value: 'tab3', label: 'Tab 3' },
];
optionsWithDisabled = [
{ value: 'enabled1', label: 'Enabled' },
{ value: 'disabled1', label: 'Disabled', disabled: true },
{ value: 'enabled2', label: 'Enabled' },
{ value: 'disabled2', label: 'Disabled', disabled: true },
];
}