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.
new tab
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);
Usage
Icon lumoIcon = new Icon("lumo", "photo");
Icon vaadinIcon = new Icon(VaadinIcon.PHONE);
add(lumoIcon, vaadinIcon);
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.
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 for reference.
Optionally, you can create a Java class/enum as a type-safe API for your Flow applications.
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.
new tab
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>
<vaadin-icon icon="${jsName}:${set[0].name.split('.')[0]}"></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 = '';
}
}
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 as reference, which use the Line Awesome icon set.
For PNG icons, use the <img>
/Image
component.
47B97C93-9646-4D2A-882F-C4F709D3D099