> Markdown version of [Icons](https://vaadin.com/docs/v23/components/icons). Section index: [llms.txt](https://vaadin.com/docs/v23/components/llms.txt)

# Icons

Vaadin comes with two built-in icon sets:

- Lumo Icons

  The Lumo Icons are designed to fit in with the default Lumo theme style. They are drawn on a 24×24 pixel canvas with a 16×16 pixel active area for the icon itself and a safe area of 4 pixels around the icon, allowing for better visual balance across the icons.

- Vaadin Icons

  The Vaadin Icons are drawn on a 16×16 pixel canvas, and have no safe area around the icon. By default, the `Icon`/`<vaadin-icon>` component renders icons on a 24×24 pixel canvas, so Vaadin Icons are scaled up and look bigger than the Lumo icons.

`icons-preview.ts`

```typescript
import '@vaadin/icon';
import '@vaadin/icons';
import '@vaadin/vaadin-lumo-styles/vaadin-iconset';
import { Iconset } from '@vaadin/icon/vaadin-iconset.js';

const DEPRECATED_ICONS: Record<string, string> = {
  'vaadin:buss': 'vaadin:bus',
  'vaadin:palete': 'vaadin:palette',
  'vaadin:funcion': 'vaadin:function',
  'vaadin:megafone': 'vaadin:megaphone',
  'vaadin:trendind-down': 'vaadin:trending-down',
};

type VaadinIconset = Iconset & { _icons: string[] };

export class IconsPreview extends HTMLElement {
  connectedCallback() {
    const lumoIconset = Iconset.getIconset('lumo') as VaadinIconset;
    const vaadinIconset = Iconset.getIconset('vaadin') as VaadinIconset;

    // A hack to get the `_icons` property computed.
    // https://github.com/vaadin/web-components/blob/447e95e0e08d396167af9a42f68b04529b412ebd/packages/vaadin-icon/src/vaadin-iconset.js#L90
    lumoIconset.applyIcon('');
    vaadinIconset.applyIcon('');

    let iconNames = Object.keys(lumoIconset._icons).map((name) => `lumo:${name}`);
    iconNames = iconNames.concat(Object.keys(vaadinIconset._icons).map((name) => `vaadin:${name}`));

    this.classList.add('icons-preview');
    let html = `
      <style>
        .icons-preview {
          display: flex !important;
          flex-direction: column;
          align-items: center;
          border: 1px solid var(--docs-divider-color-1);
          border-radius: var(--docs-border-radius-l);
        }

        .icons-preview ul {
          display: grid;
          list-style: none;
          grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
          width: 100%;
          max-height: 60vh;
          margin: 0;
          padding: 1px;
          overflow: auto;
        }

        .icons-preview li {
          display: block;
        }

        .docs-icon-preview {
          text-align: center;
          padding-bottom: var(--docs-space-l);
          line-height: 1;
        }

        .docs-icon-preview vaadin-icon {
          outline: 1px dashed var(--docs-divider-color-2);
          margin-bottom: 0.5em;
        }

        .docs-icon-preview.deprecated {
          text-decoration: line-through;
        }

        .docs-icon-preview.hidden {
          display: none;
        }

        .docs-icon-preview-name {
          display: block;
          font-size: var(--docs-font-size-2xs);
          color: var(--docs-secondary-text-color);
        }

        .docs-icon-search {
          flex: none;
          max-width: 20em;
          margin: var(--docs-space-m) auto;
          font: inherit;
          font-size: var(--docs-font-size-m);
          border: 1px solid var(--docs-divider-color-2);
          background: var(--docs-surface-color-1);
          color: var(--docs-body-text-color);
          border-radius: var(--docs-border-radius-m);
          padding: var(--docs-space-xs) var(--docs-space-s);
        }
      </style>

      <input class="docs-icon-search" type="search" aria-label="Search all icons" placeholder="Search all icons">
      <ul>
    `;

    iconNames.forEach((name: string) => {
      let title = '';
      const isDeprecated = name in DEPRECATED_ICONS;

      if (isDeprecated) {
        title = `Since Vaadin 21, '${name}' is deprecated. Use '${DEPRECATED_ICONS[name]}' instead.`;
      }

      html += `
        <li
          class="docs-icon-preview icon-${name} ${isDeprecated ? 'deprecated' : ''}"
          title="${title}"
        >
          <vaadin-icon icon="${name}"></vaadin-icon>
          <span class="docs-icon-preview-name">${name}</div>
        </li>`;
    });

    html += '</ul>';

    this.innerHTML = html;

    const search = this.querySelector('input');
    search?.addEventListener('input', () => {
      this.querySelectorAll('.docs-icon-preview').forEach((icon) => {
        icon.classList.toggle(
          'hidden',
          !icon.className.toLowerCase().includes(search.value.toLowerCase())
        );
      });
    });
  }
}

customElements.define('icons-preview', IconsPreview);
```

## <a id="usage"></a>Usage

**Java**

```java
Icon lumoIcon = new Icon("lumo", "photo");
Icon vaadinIcon = new Icon(VaadinIcon.PHONE);

add(lumoIcon, vaadinIcon);
```

**TypeScript**

```typescript
return html`
  <vaadin-icon icon="lumo:photo"></vaadin-icon>
  <vaadin-icon icon="vaadin:phone"></vaadin-icon>
`;
```

The icons are rendered as an inline `<svg>` element inside the shadow root of a `<vaadin-icon>` element, allowing you to style them using CSS.

## <a id="using-a-third-party-icon-set"></a>Using a Third-Party Icon Set

A collection of SVG files can be converted to the `<vaadin-iconset>` format, which is “consumed” by the `<vaadin-icon>`/`Icon` component. You can take a look at the [implementation of the Vaadin icon set](https://github.com/vaadin/web-components/blob/master/packages/icons/vaadin-iconset.js) for reference.

Optionally, you can create a Java class/enum as a type-safe API for your Flow applications.

### <a id="icon-set-generator"></a>Icon Set Generator

The generator converts a folder of SVG files into `<vaadin-iconset>` definitions and corresponding Java enum definitions. You can then download the files and add them to your project.

`iconset-generator.ts`

```typescript
import { css, html, LitElement, render } from 'lit';
import type { TemplateResult } from 'lit';
import { customElement, query } from 'lit/decorators.js';
import { convertToEnumName, generateVaadinIconset } from './iconset-helpers';

const capitalize = (s: string) => s && s[0].toUpperCase() + s.slice(1);

@customElement('iconset-generator')
export class IconsetGenerator extends LitElement {
  static override styles = css`
    :host {
      margin: var(--docs-space-xl) 0;
      background-color: var(--docs-surface-color-2);
      border-radius: var(--docs-border-radius-l);
      padding: var(--docs-space-l);
    }

    .input {
      display: flex;
      align-items: center;
      gap: var(--docs-space-s);
    }

    label {
      font-weight: var(--docs-font-weight-emphasis);
    }

    .name {
      -webkit-appearance: none;
      border: 1px solid var(--docs-divider-color-2);
      border-radius: var(--docs-border-radius-l);
      background-color: var(--docs-background-color);
      padding: 0.5em;
      font: inherit;
      color: inherit;
      margin: var(--docs-space-s) 0;
      height: 2.5rem;
      box-sizing: border-box;
      flex-shrink: 1;
      min-width: 5em;
    }

    small {
      color: var(--docs-secondary-text-color);
    }

    .drop {
      display: inline-flex;
      border-radius: var(--docs-border-radius-l);
      align-items: center;
      justify-content: center;
      position: relative;
      font-weight: var(--docs-font-weight-strong);
      color: var(--docs-surface-color-2);
      background-color: var(--docs-link-color);
      cursor: pointer;
      height: 2.5rem;
      padding: 0.5em 1em;
      box-sizing: border-box;
      flex: none;
    }

    .drop:focus-within {
      box-shadow: 0 0 0 2px var(--docs-surface-color-2), 0 0 0 4px var(--docs-link-color);
    }

    .drop:hover,
    :host(.drop-active) .drop {
      background-color: var(--docs-visited-link-color);
    }

    .drop::before {
      content: 'Upload icons';
    }

    .drop input {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      margin: 0;
      opacity: 0;
      width: 100%;
      cursor: inherit;
    }

    .output textarea {
      -webkit-appearance: none;
      width: 100%;
      height: 10em;
      background: transparent;
      font: inherit;
      color: inherit;
      font-family: var(--docs-font-family-monospace);
      border: 0;
      padding: var(--docs-space-m);
      margin: 0;
      resize: vertical;
      pointer-events: auto;
      box-sizing: border-box;
    }

    .output ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }

    .output li {
      border: 2px solid var(--docs-divider-color-1);
      border-radius: var(--docs-border-radius-l);
      margin: var(--docs-space-s) 0;
      position: relative;
    }

    .output li details {
      font-size: var(--docs-font-size-xs);
      pointer-events: none;
      margin-top: -2em;
    }

    .output summary {
      cursor: pointer;
      padding: 0 var(--docs-space-m);
      padding-bottom: var(--docs-space-s);
      width: max-content;
      pointer-events: auto;
      color: var(--docs-secondary-text-color);
    }

    .output summary:hover,
    .output summary:focus-visible {
      color: var(--docs-heading-text-color);
    }

    .output li a {
      color: var(--docs-link-color);
      font-weight: var(--docs-font-weight-strong);
      text-decoration: none;
      display: block;
      padding: var(--docs-space-s) var(--docs-space-m);
      padding-bottom: 2em;
    }

    .output li a:hover::before {
      content: '';
      position: absolute;
      text-align: end;
      top: -2px;
      left: -2px;
      right: -2px;
      bottom: -2px;
      border: 2px solid var(--docs-link-color);
      border-radius: var(--docs-border-radius-l);
      pointer-events: none;
    }

    .output li a:hover::after {
      content: '↓';
      position: absolute;
      height: 1em;
      width: 2em;
      top: 1.25em;
      right: 0;
      pointer-events: none;
      font-size: 1.25em;
      line-height: 1;
    }

    .output details :is(p, label) {
      padding: 0 var(--docs-space-m);
      margin-top: 0;
    }
  `;

  @query('.output')
  private output!: HTMLElement;

  @query('.name')
  private nameInput!: HTMLInputElement;

  protected override render() {
    return html`
      <label for="iconsetname">Icon set name</label><br />
      <small>Use CamelCase naming. Leave empty to use folder name(s) only.</small><br />
      <div class="input">
        <input type="text" value="MyIcons" class="name" id="iconsetname" />
        <div class="drop">
          <input
            type="file"
            webkitdirectory
            directory
            multiple
            @dragover=${this.handleDragOver}
            @dragleave=${this.handleDragLeave}
            @drop=${this.handleDragLeave}
            @change=${this.handleFiles}
          />
        </div>
      </div>
      <small>Upload a folder, or nested folders, of <code>.svg</code> files.</small>
      <div class="output"></div>
    `;
  }

  handleDragOver(e: DragEvent) {
    e.preventDefault();
    this.classList.add('drag-active');
  }

  handleDragLeave() {
    this.classList.remove('drag-active');
  }

  async handleFiles(event: Event) {
    const dropzone = event.target as HTMLInputElement;

    const iconsets: Record<string, File[]> = {};
    const name = this.nameInput.value;
    let folderName = '';

    // Categorize into sets based on folder path
    const files = dropzone.files ?? [];
    [...files].forEach((f) => {
      if (f.webkitRelativePath) {
        const parts = f.webkitRelativePath.split(/\/|\\/);
        folderName = parts[parts.length - 2].toLowerCase();
      }

      const iconsetName = folderName || '';

      let set = iconsets[iconsetName];
      if (!set) {
        iconsets[iconsetName] = [];
        set = iconsets[iconsetName];
      }
      set.push(f);
    });

    const promises = Object.entries(iconsets).map(
      async ([iconsetName, set]): Promise<TemplateResult> => {
        // Sort alphabetically
        set.sort((a, b) => {
          if (a.name < b.name) {
            return -1;
          }
          if (a.name > b.name) {
            return 1;
          }
          return 0;
        });

        let enumName = name + capitalize(iconsetName);
        if (!enumName) {
          enumName = 'Icons';
        }

        const jsName = enumName.replace(/([a-z])([A-Z]+)/g, '$1-$2').toLowerCase();

        // Generate <vaadin-iconset> JS import and Java enum class
        const iconsetStrings = await generateVaadinIconset(set, jsName, enumName);

        const jsBlob = new Blob([iconsetStrings.js], { type: 'text/plain' });
        const javaBlob = new Blob([iconsetStrings.java], { type: 'text/plain' });

        return html`
          <li>
            <a download="${jsName}.js" .href=${URL.createObjectURL(jsBlob)}>${jsName}.js</a>
            <details>
              <summary>Usage and contents</summary>
              <p>
                <b>Usage:</b>
                <code>
                  &lt;vaadin-icon icon="${jsName}:${set[0].name.split('.')[0]}">&lt;/vaadin-icon>
                </code>
              </p>
              <label for="jsOutput"><b>File contents:</b></label>
              <textarea readonly id="jsOutput">${iconsetStrings.js}</textarea>
            </details>
          </li>
          <li>
            <a download="${enumName}.java" .href=${URL.createObjectURL(javaBlob)}>
              ${enumName}.java
            </a>
            <details>
              <summary>Usage and contents</summary>
              <p>
                <b>Usage:</b>
                <code>${enumName}.${convertToEnumName(set[0].name.split('.')[0])}.create();</code>
              </p>
              <label for="javaOutput"><b>File contents:</b></label>
              <textarea readonly id="javaOutput">${iconsetStrings.java}</textarea>
            </details>
          </li>
        `;
      }
    );

    const outputHtml = await Promise.all(promises);

    const plural = Object.keys(iconsets).length > 1;

    const tFiles = plural ? 'files are' : 'file is';
    const tThem = plural ? 'them' : 'it';

    render(
      html`
        <p>
          Download the following files. The <code>.js</code> ${tFiles} required.
          Place ${tThem} into the <code>frontend/icons/</code> folder.
        </p>
          The <code>.java</code> ${tFiles} optional.
          Place ${tThem} under the <code>src/</code> folder (you are free to choose the Java
          package).
        </p>
        <ul>
          ${outputHtml}
        </ul>
      `,
      this.output
    );

    dropzone.value = '';
  }
}
```

### <a id="other-icon-formats"></a>Other Icon Formats

Third-party icon sets come in many different formats, and there is no single integration method that works for every format.

For **SVG icons**, you can use the generator on this page.

For **font icons**, you can use projects generated by [Vaadin Start](https://start.vaadin.com) as reference, which use the [Line Awesome](https://icons8.com/line-awesome/) icon set.

For **PNG icons**, use the `<img>`/[`Image`](https://vaadin.com/docs/v23/application/resources.md#the-image-component) component.

`47B97C93-9646-4D2A-882F-C4F709D3D099`
