Components: mat-icon shows text before showing properly

Created on 12 Jul 2018  路  19Comments  路  Source: angular/components

Bug, feature request, or proposal:

Bug or feature request.

What is the expected behavior?

When using <mat-icon>home</mat-icon>, nothing should be displayed until the icon is loaded properly. As soon as the icon is loaded, it should render.

What is the current behavior?

Depending on the speed of your connection, it is possible, that you see the text (for example home) instead of the icon.

What are the steps to reproduce?

  1. Start the Angular Material Icon example in Stackblitz.
    It might be better to open it in it's own tab.
  2. Open the Developer-Tools (in Chrome) and under the Network-Tab choose a very slow network throttling profile. This is needed, as the sample app is very small and therefore the problem is not noticeable with normal connections.
  3. Reload the page and wait until it is loaded. You will see something like this:
    image
    After some time it shows the icon:
    image

What is the use-case or motivation for changing an existing behavior?

In an app I am currently working on, this happens pretty often. Even if I only change the route (which does not trigger a reload of the page) I often see the text of the icons before the icon is displayed properly, so it does not seem to be related to the loading of the icon font.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

I am using @angular/material 6.3.1 together with TypeScript 2.7.2 on a Windows 10 PC.
The problem occures at least on Chrome, Opera, Firefox and Edge.

P4 materiaicon feature

Most helpful comment

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts)
https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons
In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

All 19 comments

I just noticed, that the problem in my app was actually the change-detection. I am using CdkPortal and CdkPortalOutlet to dynamicaly change the ocntent of my app-bar (including some mat-icons).
As I am using ChangeDetectionStrategy.OnPush, I had to call detectChanges() after changing the content.
However, you can still see the text, while the font is loading. On normal connections it isn't really noticeable, but if you have a slow connection, the text stays for a while.
I guess it would be good, if the icons don't show anything as long as the used font is not fully loaded.
Not sure, if thats even possible...

Going to close this as "working as expected". The component on its own doesn't have a good way of knowing when the icon font is loaded. If your app knows when the assets are loaded, you can hide the icons until then, or you can potentially fall back to pngs or svgs.

@jelbourn would you reopen this if the component does in fact have a good way of knowing when the font is loaded?

if ("fonts" in document && !document.fonts.check(iconFontName)){
  document.body.classList.add("mat-icon-font-missing");
  document.fonts.load(iconFontName)
    .then(() => document.body.classList.remove("mat-icon-font-missing"));
}

CSS should be easy enough once you have that. Of course, this doesn't avoid the FOUT for IE/Edge users, but there's just no helping some people.

@thw0rted Interesting, I didn't know about that API. We would need some way to know the name of the icon-font being used, since it isn't always necessarily the Material Design icon font. I imagine this would become a new optional setting on the icon registry. It's something that would be good for a community PR.

Is the icon registry responsible for the default behavior today? The docs say "By default, expects the Material icons font" with a link to instructions on including the @font-face definition. I was going to suggest a universal / top-level "icon font not loaded" class, but of course with the registry you can have an arbitrary number of fontSet so you'd need to do the above logic for each font in use.

And I see what you were saying in your original response, none of the current registry methods seem to care about the font name. Moreover, it appears to be possible to use font-based mat-icon without touching the registry at all -- I haven't done it myself, but it sounds like you just need to set the fontSet / fontIcon params. I guess this sort of behavior would need to either be added to a current call (like an extra parameter for registerFontClassAlias) or just be exposed as a new method.

Yeah, the registry has the default icon font css class. So in addition to that class, it would need to know about the font name.

Hi there,

document.body.classList.add("mat-icon-font-missing");
document.fonts.load(iconFontName)

Please keep in mind that it might break universal if you refer to classList

When you say "break universal", I guess technically Material2 could support IE9 but that's the only outlier I see on the list and frankly, if you're stuck on IE9 you have bigger problems in your life 馃槖

just checking if there is any PR WIP on this?

When you say "break universal", I guess technically Material2 could support IE9 but that's the only outlier I see on the list and frankly, if you're stuck on IE9 you have bigger problems in your life 馃槖

Not sure about IE9, universal is server side rendering ;-) fortunately I am not stuck with IE9 aahahah

I think I got the same issue: mat-icon displays only text when using OnPush in the parent component. When OnPush is removed everything works.

Here's a Stackblitz example with angular 8 an angular material 8: https://stackblitz.com/edit/components-issue-ghjcpf?file=app/app.component.ts

Explanation of the code:
I have a custom IfDirective (which I have modified to show the problem). The directive changes every 2 seconds its ViewContainer contents.

What you should be able to see:
The first 2 seconds you can see the notifications icon. After those 2 seconds the directive will clear the view container and displays the other template. This other template also contains a mat-icon, but it will always display the string instead of the icon.

When you remove changeDetection: ChangeDetectionStrategy.OnPush from the AppComponent everything works as expected. But that's no solution.

When you change the directive input variable show from within the AppComponent everything is working fine including OnPush, because in this case the component automatically runs the change detection, I guess.

The mat-raised-button with text behave normal. Animations, text, styles are in place. The problem only happens with mat-icon.

EDIT: "my" problem can be fixed by calling detectChanges() within the directive:

this.viewContainer.createEmbeddedView(this.templateRef).detectChanges();

EDIT2: Another "solution" (or workaround) that works for me, is this:

ngOnInit() {
    const ifTemplateRef = this.templateRef.createEmbeddedView(null);
    const elseTemplateRef = this.appIfAuthenticatedElse.createEmbeddedView(null);

    this.someObservable$.subscribe(val => {
        this.viewContainer.detach();
        if (val) {
            this.viewContainer.insert(ifTemplateRef);
        } else if (this.appIfAuthenticatedElse) {
            this.viewContainer.insert(elseTemplateRef);
        }
    });
}

Is there a requirement for the font ligature identifier to be provided as the text inside the <mat-icon> tag? I think you can introduce an attribute for this function, just like the svgIcon or fontIcon attributes, and the html-rendering issue would be solved. e.g.:

<mat-icon ligature="home"></mat-icon>

Here's how I am solving it, with fontfaceobserver:

app.component.ts

export class AppComponent {
  constructor(renderer: Renderer2) {
    const materialIcons = new FontFaceObserver('Material Icons');
    materialIcons.load(null, 10000)
      .then(() => this.renderer.addClass(document.body, `${keyword}-loaded`))
      .catch(() => this.renderer.addClass(document.body, `${keyword}-error`)); // this line not necessary for simple example
  }
}

Simple - don't show until font is loaded

scss

// don't show mat-icon until font is loaded, unless using a different font set
:not(.material-icons-loaded) .mat-icon:not([fontSet]) {
  display: none;
}

Fancy - allow fallback text if font load fails

scss

.mat-icon:not([fontSet]) {
  display: none;
  .#{$class}-loaded & {
    // loaded font successfully
    display: inline-block;
  }
  .#{$class}-error & {
    // could not load font - show fallback label if available
    display: inline-block;
    visibility: hidden;
    &[data-label]:before {
      content: attr(data-label);
      visibility: visible;
    }
  }
}

html

<!-- displays nothing -->
<mat-icon>arrow_forward</mat-icon>

<!-- displays 'submit' -->
<mat-icon [data-label]="submit">arrow_forward</mat-icon>

I am working with an Angular 8 project where I am utilizing mat-icon's that were experiencing FOUT (Flash of Unstyled Text) resulting in the icon text getting displayed for a brief moment while the Web Application waited for the Material Icon font families to load. My solution was to add the npm package webfontloader

npm install --save webfontloader

Inside the app.module.ts:

import * as WebFont from 'webfontloader';

WebFont.load({
  custom: { families: ['Material Icons', 'Material Icons Outline'], }
});

And in the css/sass file where the font families are imported:

.wf-loading body {
  display: none;
}

Update: I'm not so sure my suggestion below actually works!

I think what may have happened is that Firefox specifically still flashes icons during the SSR handover, while Chrome doesn't.

Just in case, here's my original comment about what I thought would fix that with Universal:


Thanks for the discussion all, and suggested workarounds @cbejensen + @thekarlbrown.

I was seeing this issue in an Angular 9 Universal app, I think most visibly on even faster connections during the handover from server to client JS (possibly a side effect / deeper issue around where things load from, but this was at least the most visible symptom).

I had to adapt these solutions to get server renders working. I tried the Typekit webfontloader first but found it had a window dependency, at least when loaded with import, which broke the SSR build.

Using fontfaceobserver got round this problem, but giving Renderer2 a document.body ref led to a runtime crash on the server side, for (in retrospect) obvious reasons.

So my approach is now based on @cbejensen's but hiding the app component instead of the body, and using ElementRef:

app.component.ts:

import { ElementRef, Inject, OnInit, Renderer2 } from '@angular/core';
import * as FontFaceObserver from 'fontfaceobserver';
// ...
export class AppComponent implements OnInit {
  constructor(
    private elementRef: ElementRef,
    @Inject(PLATFORM_ID) private platformId: Object,
    private renderer: Renderer2,
    // ...
  ) {}

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      // Avoid flash of icon placeholder text during SSR -> client JS handover.
      const materialIcons = new FontFaceObserver('Material Icons');
      materialIcons.load(null, 10000)
        .then(() => this.renderer.addClass(this.elementRef.nativeElement, 'material-icons-loaded'));

      // ...existing client-side-only inits.
    } else {
      // On the server side, add the class to show icons immediately.
      this.renderer.addClass(this.elementRef.nativeElement, 'material-icons-loaded');
    }
  }
}
...

app.component.scss:

...
app-root:not(.material-icons-loaded) {
  display: none;
}
...

Hope this helps anyone else trying to sort this with a Universal / dynamic SSR build.

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts)
https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons
In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts)
https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons
In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

Work for me !

Would be nice to allow hide text in case of failure.

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts)
https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons
In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

Update on two things

  1. Google fonts support the font-display property in the URL itself. So, self-hosting is not mandatory.

https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round&display=block"

  1. font-display: block gives only a short period of block after that it works like swap. which means, if you can throttle the network to slow 3G, you still see FOUT after some time.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

vanor89 picture vanor89  路  3Comments

RoxKilly picture RoxKilly  路  3Comments

savaryt picture savaryt  路  3Comments

crutchcorn picture crutchcorn  路  3Comments

Miiekeee picture Miiekeee  路  3Comments