# Icon

The icon component resolves symbols from the generated sprite and falls back across size and variant combinations when needed. For most product UI, icons stay decorative and inherit color from surrounding content. The key practical need in this showcase is the browser with search, because the icon catalog is large enough that static previews alone are not useful.

## Import
```ts
import { IconComponent, type IconName, ALL_ICON_NAMES } from 'ui';
```

## Overview
```ts
import { Component } from '@angular/core';
import { IconComponent, IconName } from 'ui';

type IconVariant = 'regular' | 'filled';
type IconSize = 'small' | 'medium' | 'large';

const overviewIcons: IconName[] = ['home', 'search', 'settings', 'info', 'checkmark', 'delete'];
const sizes: IconSize[] = ['small', 'medium', 'large'];
const variants: { label: string; value: IconVariant }[] = [
  { label: 'Regular', value: 'regular' },
  { label: 'Filled', value: 'filled' },
];

@Component({
  selector: 'app-icon-overview-demo',
  standalone: true,
  imports: [IconComponent],
  template: `
    <div style="display:grid;gap:1rem;max-width:56rem;">
      @for (variant of variants; track variant.value) {
        <section style="display:grid;gap:0.75rem;">
          <strong style="font-size:0.875rem;">{{ variant.label }}</strong>
          <div style="display:grid;gap:0.75rem;">
            @for (iconName of icons; track iconName) {
              <div
                style="display:flex;flex-wrap:wrap;align-items:center;gap:0.875rem 1rem;padding:0.875rem 1rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 60%,transparent);border-radius:12px;background:var(--color-neutral-background-rest);"
              >
                <strong style="min-width:6rem;font-size:0.875rem;">{{ iconName }}</strong>
                @for (size of sizes; track size) {
                  <div
                    style="display:grid;justify-items:center;gap:0.375rem;min-width:4.5rem;padding:0.5rem 0.625rem;border-radius:10px;background:var(--color-neutral-background2-rest);"
                  >
                    <ui-icon [icon]="iconName" [size]="size" [variant]="variant.value" />
                    <span
                      style="font-size:0.75rem;line-height:1.2;color:var(--color-neutral-foreground2-rest);text-transform:capitalize;"
                    >
                      {{ size }}
                    </span>
                  </div>
                }
              </div>
            }
          </div>
        </section>
      }
    </div>
  `,
})
export class IconOverviewDemoComponent {
  protected readonly icons = overviewIcons;
  protected readonly sizes = sizes;
  protected readonly variants = variants;
}
```

## Sizes and custom pixels
```ts
import { Component } from '@angular/core';
import { IconComponent, Size } from 'ui';

const sizes: Array<{ size: Size; label: string }> = [
  { size: 'small', label: 'Small' },
  { size: 'medium', label: 'Medium' },
  { size: 'large', label: 'Large' },
];

@Component({
  selector: 'app-icon-size-demo',
  standalone: true,
  imports: [IconComponent],
  template: `
    <div
      style="display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));max-width:44rem;"
    >
      @for (item of sizes; track item.size) {
        <div
          style="display:grid;gap:0.75rem;justify-items:center;padding:1rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 60%,transparent);border-radius:12px;background:var(--color-neutral-background-rest);"
        >
          <ui-icon icon="calendar_clock" [size]="item.size" />
          <strong style="font-size:0.875rem;">{{ item.label }}</strong>
          <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest);">
            preset {{ item.size }}
          </span>
        </div>
      }

      <div
        style="display:grid;gap:0.75rem;justify-items:center;padding:1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);"
      >
        <ui-icon icon="calendar_clock" [sizePx]="32" />
        <strong style="font-size:0.875rem;">Custom</strong>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest);">
          sizePx="32"
        </span>
      </div>
    </div>
  `,
})
export class IconSizeDemoComponent {
  protected readonly sizes = sizes;
}
```

## Variants and rotation
```ts
import { Component } from '@angular/core';
import { IconComponent } from 'ui';

@Component({
  selector: 'app-icon-variant-demo',
  standalone: true,
  imports: [IconComponent],
  template: `
    <div
      style="display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));max-width:40rem;"
    >
      <div
        style="display:grid;gap:0.75rem;justify-items:center;padding:1rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 60%,transparent);border-radius:12px;background:var(--color-neutral-background-rest);"
      >
        <ui-icon icon="star" size="large" variant="regular" />
        <strong style="font-size:0.875rem;">Regular</strong>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest);">
          Best for lower visual weight
        </span>
      </div>

      <div
        style="display:grid;gap:0.75rem;justify-items:center;padding:1rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 60%,transparent);border-radius:12px;background:var(--color-neutral-background-rest);"
      >
        <ui-icon icon="star" size="large" variant="filled" />
        <strong style="font-size:0.875rem;">Filled</strong>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest);">
          Better for emphasis and active states
        </span>
      </div>

      <div
        style="display:grid;gap:0.75rem;justify-items:center;padding:1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);"
      >
        <ui-icon icon="arrow_clockwise_dashes_settings" size="large" [rotate]="90" />
        <strong style="font-size:0.875rem;">Rotation</strong>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest);">
          Optional rotate for special cases
        </span>
      </div>
    </div>
  `,
})
export class IconVariantDemoComponent {}
```

## Decorative and semantic usage
```ts
import { Component } from '@angular/core';
import { BadgeComponent, ButtonComponent, IconComponent } from 'ui';

@Component({
  selector: 'app-icon-semantic-demo',
  standalone: true,
  imports: [IconComponent, ButtonComponent, BadgeComponent],
  template: `
    <section
      style="display:grid;gap:1rem;max-width:48rem;padding:1.25rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 70%,transparent);border-radius:1rem;background:color-mix(in srgb,var(--color-neutral-background-rest) 92%,white);"
    >
      <div style="display:grid;gap:0.25rem;">
        <h3 style="margin:0;font-size:1rem;">Decorative and semantic usage</h3>
        <p style="margin:0;font-size:0.875rem;color:var(--color-neutral-foreground2-rest);">
          Most icons are decorative. Add ariaLabel only when the icon itself needs to be announced
          as meaningful content.
        </p>
      </div>

      <div style="display:flex;flex-wrap:wrap;gap:0.875rem;">
        <ui-button text="Search workspace" icon="search" />
        <ui-button text="Settings" icon="settings" variant="secondary" />
        <ui-badge text="Synced" icon="checkmark" variant="success" />
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);"
      >
        <div style="display:flex;align-items:center;gap:0.5rem;">
          <ui-icon icon="info" ariaLabel="Informational status" />
          <span style="font-size:0.875rem;">Semantic icon with ariaLabel</span>
        </div>
        <div style="display:flex;align-items:center;gap:0.5rem;">
          <ui-icon icon="info" />
          <span style="font-size:0.875rem;">Decorative icon without extra announcement</span>
        </div>
      </div>
    </section>
  `,
})
export class IconSemanticDemoComponent {}
```

## Icon browser with search
```ts
import { Component, ElementRef, computed, effect, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  ALL_ICON_NAMES,
  DropdownComponent,
  DropdownItem,
  IconComponent,
  IconName,
  SearchComponent,
  Size,
} from 'ui';

const ICON_BROWSER_BATCH_SIZE = 120;
const variants = ['regular', 'filled'] as const;
const sizes: Size[] = ['small', 'medium', 'large'];
const sizeItems: DropdownItem[] = sizes.map(size => ({
  value: size,
  label: size[0].toUpperCase() + size.slice(1),
}));
const variantItems: DropdownItem[] = variants.map(variant => ({
  value: variant,
  label: variant[0].toUpperCase() + variant.slice(1),
}));

@Component({
  selector: 'app-icon-browser-demo',
  standalone: true,
  imports: [FormsModule, DropdownComponent, IconComponent, SearchComponent],
  host: {
    style: 'display:block;width:100%;',
  },
  template: `
    <section style="display:grid;gap:1rem;width:100%;max-width:56rem;margin-inline:auto;">
      <div
        style="display:grid;gap:0.875rem;justify-items:center;width:100%;max-width:40rem;margin-inline:auto;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);"
      >
        <div
          style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));align-items:end;gap:0.75rem;width:100%;"
        >
          <div style="min-width:0;">
            <ui-search
              label="Search icons"
              style="display:block;width:100%;"
              placeholder="Search icons..."
              [(ngModel)]="searchQueryValue"
              [ngModelOptions]="{ standalone: true }"
            />
          </div>

          <div style="min-width:0;">
            <ui-dropdown
              label="Size"
              placeholder="Choose size"
              [items]="sizeItems"
              [ngModel]="browserSize()"
              (ngModelChange)="browserSize.set(asSize($event))"
              [ngModelOptions]="{ standalone: true }"
            />
          </div>

          <div style="min-width:0;">
            <ui-dropdown
              label="Variant"
              placeholder="Choose variant"
              [items]="variantItems"
              [ngModel]="browserVariant()"
              (ngModelChange)="browserVariant.set(asVariant($event))"
              [ngModelOptions]="{ standalone: true }"
            />
          </div>
        </div>
      </div>

      <div style="font-size:0.875rem;color:var(--color-neutral-foreground2-rest);">
        Showing <strong>{{ visibleIcons().length }}</strong> of {{ filteredIcons().length }}
        matching icons
        @if (filteredIcons().length < iconNames.length) {
          <span> from {{ iconNames.length }} available icons</span>
        }
      </div>

      @if (copiedIcon()) {
        <div
          style="padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);font-size:0.875rem;"
        >
          Copied icon name: <strong>{{ copiedIcon() }}</strong>
        </div>
      }

      @if (filteredIcons().length > 0) {
        <div
          #scrollContainer
          (scroll)="onGridScroll($event)"
          style="display:grid;grid-template-columns:repeat(auto-fill,minmax(6.75rem,1fr));gap:0.75rem;max-height:34rem;overflow:auto;padding:0.25rem;"
        >
          @for (iconName of visibleIcons(); track iconName) {
            <button
              type="button"
              (click)="copyIconName(iconName)"
              [title]="iconName"
              style="display:grid;gap:0.625rem;justify-items:center;padding:0.875rem 0.75rem;border:1px solid color-mix(in srgb,var(--color-neutral-stroke-rest) 78%,transparent);border-radius:12px;background:var(--color-neutral-background-rest);box-shadow:0 1px 2px color-mix(in srgb,#000 6%,transparent);cursor:pointer;text-align:center;"
            >
              <ui-icon [icon]="iconName" [size]="browserSize()" [variant]="browserVariant()" />
              <span
                style="font-size:0.75rem;line-height:1.25;color:var(--color-neutral-foreground2-rest);word-break:break-word;"
              >
                {{ iconName }}
              </span>
            </button>
          }
        </div>
      } @else {
        <div
          style="padding:1.25rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:12px;background:var(--color-neutral-background2-rest);font-size:0.875rem;"
        >
          No icons found matching "{{ searchQueryValue }}".
        </div>
      }
    </section>
  `,
})
export class IconBrowserDemoComponent {
  protected readonly iconNames = ALL_ICON_NAMES as IconName[];
  protected readonly sizes = sizes;
  protected readonly variants = variants;
  protected readonly sizeItems = sizeItems;
  protected readonly variantItems = variantItems;

  private readonly searchQuery = signal('');
  private readonly visibleIconsCount = signal(ICON_BROWSER_BATCH_SIZE);
  protected readonly browserSize = signal<Size>('medium');
  protected readonly browserVariant = signal<(typeof variants)[number]>('regular');
  protected readonly copiedIcon = signal<IconName | null>(null);

  private readonly scrollContainer = viewChild<ElementRef<HTMLDivElement>>('scrollContainer');

  get searchQueryValue(): string {
    return this.searchQuery();
  }

  set searchQueryValue(value: string) {
    this.searchQuery.set(value);
  }

  protected readonly filteredIcons = computed<IconName[]>(() => {
    const query = this.searchQuery().toLowerCase().trim();
    if (!query) {
      return this.iconNames;
    }
    return this.iconNames.filter(iconName => iconName.toLowerCase().includes(query)) as IconName[];
  });

  protected readonly visibleIcons = computed<IconName[]>(() =>
    this.filteredIcons().slice(0, this.visibleIconsCount()),
  );

  private readonly hasMoreVisibleIcons = computed<boolean>(
    () => this.visibleIcons().length < this.filteredIcons().length,
  );

  constructor() {
    effect(() => {
      this.searchQuery();
      this.visibleIconsCount.set(ICON_BROWSER_BATCH_SIZE);

      queueMicrotask(() => {
        const container = this.scrollContainer()?.nativeElement;
        if (container) {
          container.scrollTop = 0;
        }
      });
    });
  }

  protected onGridScroll(event: Event): void {
    const container = event.target as HTMLDivElement | null;
    if (!container || !this.hasMoreVisibleIcons()) {
      return;
    }

    const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
    if (distanceToBottom <= 320) {
      this.visibleIconsCount.update(count => count + ICON_BROWSER_BATCH_SIZE);
    }
  }

  protected async copyIconName(iconName: IconName): Promise<void> {
    try {
      await navigator.clipboard.writeText(iconName);
    } catch {
      const textArea = document.createElement('textarea');
      textArea.value = iconName;
      textArea.style.position = 'fixed';
      textArea.style.opacity = '0';
      document.body.appendChild(textArea);
      textArea.select();
      document.execCommand('copy');
      document.body.removeChild(textArea);
    }

    this.copiedIcon.set(iconName);
  }

  protected asSize(value: string | number | null | undefined): Size {
    return value === 'small' || value === 'large' ? value : 'medium';
  }

  protected asVariant(value: string | number | null | undefined): (typeof variants)[number] {
    return value === 'filled' ? 'filled' : 'regular';
  }
}
```

## Accessibility

### Decorative versus semantic icons
By default the icon stays decorative and is hidden from assistive technology. Add `ariaLabel` only when the icon itself carries meaning that is not already present in surrounding text.

| Mode | Accessibility behavior |
| --- | --- |
| decorative icon | `aria-hidden="true"` |
| semantic icon | `role="img"` with `aria-label` |
| icon inside a labeled control | usually remains decorative because the parent control already exposes the label |

### Direction and locale fallback
The component can resolve locale-specific or direction-aware symbols and also allows explicit `direction` or `locale` overrides. In most app usage this stays automatic rather than manually configured.

### Usage guidance
Do not rely on icons alone to communicate critical meaning. Pair important status or action meaning with text, tooltip content, or an already labeled parent control.
