Search code examples
web-component

FOUC problems with web component


I'm new using web component. I defined a MenuComponent:

menu-component.html:

<link rel="stylesheet" href="menu-component/menu-component.css">
<div class="mobile-menu">
    <div></div>
    <div style="width: 100%;"></div>
    <div></div>
</div>
<div class="mobile-page-title"></div>
<a href="index.html">
    <div class="logo">
        <div style="letter-spacing: .01rem; padding-right: 0.1rem;">creativ</div>
        <div class="logo-accent" style="font-size: 2.8rem; line-height: 2rem; align-self: flex-start;">3</div>
        <div class="logo-accent" style="letter-spacing: 0.1rem;">ly</div>
    </div>
</a>
<div class="menu-container">
    <div class="menu-center">
        <div id="home-menu" class="menu-item" style="margin-top: 0.5rem;">
            <a href="index.html">home</a>
        </div>
        <div class="menu-underline"></div>
    </div>
    <div class="menu-center">
        <div id="design-menu" class="menu-item first-menu">
            <a href="designs.html">designs</a>
        </div>
        <div class="menu-underline"></div>
    </div>
    <div class="slide-right2 menu-center">
        <div id="logos-menu" class="menu-item">
            <a href="logos.html">logos</a>
        </div>
        <div class="menu-underline"></div>
    </div>
    <div class="slide-right3 menu-center">
        <div id="paintings-menu" class="menu-item">
            <a href="paintings.html">paintings</a>
        </div>
        <div class="menu-underline"></div>
    </div>
    <div class="slide-right4 menu-center">
        <div id="bio-menu" class="menu-item">
            <a href="bio.html">bio</a>
        </div>
        <div class="menu-underline"></div>
    </div>
    <div class="slide-right5 menu-center">
        <div id="contacts-menu" class="menu-item">
            <a href="contacts.html">contacts</a>
        </div>
        <div class="menu-underline"></div>
    </div>
</div>

menu-component.css:

a {
    color: unset;
    text-decoration: none;
    cursor: unset;
}

.logo {
    width: fit-content;
    padding: 0 1rem;
    font-size: 1.8rem;
    display: flex;
    align-items: baseline;
    top: 2rem;
    position: fixed;
    cursor: pointer;
    z-index: 2000;
}

.logo-accent {
    color: var(--brand-color);
    font-family: var(--accent-font-family);
    font-style: italic;
    font-size: 2.37rem;
}

.mobile-page-title {
    display: none;
    color: var(--background-color);
    font-family: var(--accent-font-family);
    font-style: italic;
    font-size: 1.4rem;
    background-color: var(--brand-color);
    padding: 0.5rem 1rem 0.6rem 3rem;
    border-radius: 0 20px 20px 0;
    top: 0px;
    position: fixed;
    z-index: 2000;
}

.no-underline {
    border-bottom: 2px solid var(--brand-color);
}

.underline {
    border-bottom: 2px solid var(--complementary-color);
    transition: width 2s;
    width: 3%;
}

.menu-item {
    color: var(--background-color);
    font-family: var(--accent-font-family);
    padding-top: 1.2rem;
    cursor: pointer;
    font-style: italic;
    animation: 2s slide-right;
}

.menu-center {
    display: flex;
    flex-direction: column;
    width: 100%;
    overflow: hidden;
    align-items: center;
}

.menu-underline {
    height: 4px;
    border-top: 2px solid var(--complementary-color);
    box-sizing: border-box;
    width: 0%;
    transition: .5s;
}

.menu-item:hover + .menu-underline {
    width: 100%;
  }


.mobile-menu {
    display: none;
}

.first-menu {
    margin-top: 0.5rem;
}

#home-menu {
    display: none;
}

.menu-container {
    width: 165px;
    position: fixed;
    top: 50%;
    transform: translate(0, -50%);
    background-color: var(--brand-color);
    border-radius: 0 0.5rem 0.5rem 0;
    padding: 0 1em 1.5rem 1rem;
    font-size: 1.5rem;
    line-height: 1.5;
    box-sizing: border-box;
    letter-spacing: .03em;
}

.slide-right {
    width: 100%;
    overflow: hidden;
    text-align: center;
}

.slide-right2 div {
    animation-delay: 0.5s;
    animation-fill-mode: backwards;
}

.slide-right3 div {
    animation-delay: 1s;
    animation-fill-mode: backwards;
}

.slide-right4 div {
    animation-delay: 1.5s;
    animation-fill-mode: backwards;
}

.slide-right5 div {
    animation-delay: 2s;
    animation-fill-mode: backwards;
}


@keyframes slide-right {
    from {
        margin-left: -180%;
    }

    to {
        margin-left: 0%;
    }
}

@media only screen and (max-width: 768px) {
    /* For mobile phones: */
    
    .menu-container {
        top: 0;
        transform: none;
        display: none;
        z-index: 2002;
        animation: 1s menu-opening ease-in-out forwards;
    }

    @keyframes menu-opening {
        0%   {transform: scale(0); transform-origin: top left;}
        100% {transform: scale(1);}
      }

    .logo {
        transform: scale(0.8);
        top: 0;
        right: 0;
        /* position: absolute; */
        padding: 0;
        animation: 1s scale-logo ease-in-out;
        animation-fill-mode: backwards;
    }

    @keyframes scale-logo {
        0%   {transform: scale(0.8);}
        100% {transform: scale(1);}
      }

    .mobile-menu {
        margin: 1rem 0.5rem 0 0.5rem;
        display: flex;
        flex-direction: column;
        align-items: center;
        position: fixed;
        width: 25px;
        animation: 1s scale-mobile-menu ease-in-out;
        animation-fill-mode: backwards;
        z-index: 2000;
        justify-content: space-between;
        height: 1.2rem;
        z-index: 2001;
    }

    @keyframes scale-mobile-menu {
        0%   {transform: scale(1);}
        100% {transform: scale(1.3);}
      }
    
    .mobile-menu div {
        width: 80%;
        height: 3px;
        background-color: var(--brand-color);
        border-radius: 1px;
    }

    .menu-item {
        animation: none;
    }

    .first-menu {
        margin-top: 0;
    }

    #home-menu {
        display: block;
    }
  }

menu-component.js

fetch("menu-component/menu-component.html")
    .then(stream => stream.text())
    .then(text => define(text));

function define(html) {
    class MenuComponent extends HTMLElement {

        constructor() {
            super();

            var shadow = this.attachShadow({mode: 'open'});
            shadow.innerHTML = html;
            
            let menuContainer = shadow.querySelectorAll(".menu-container")[0];
            document.addEventListener('click', (event) => {
                if (event.target !== this) {
                  this.closeMobileMenu(menuContainer);
                }
              });
            let menuItems = shadow.querySelectorAll(".menu-item");
            for (let menuItem of menuItems) {
                menuItem.addEventListener('click', this.onMouseClick);
            }
            let mobileMenu = shadow.querySelectorAll(".mobile-menu")[0];
            mobileMenu.addEventListener('click', this.onMobileMenuClick);

            //set the menu with the name of the page if it is not home page
            const url = window.location.href;
            const index = url.lastIndexOf("/");
            const urlPage = url.substring(index+1);
            let bodyWidth = document.body.clientWidth;
            console.log(bodyWidth);
            if (!urlPage.includes('index') && urlPage !== '') {
                let pageTitle = shadow.querySelectorAll(".mobile-page-title")[0];
                if (bodyWidth <= 750) {
                    let pageNameExt = url.split("/").pop();
                    let pageName = pageNameExt.split(".")[0];
                    if (pageName.includes("design")) {
                        pageName = "designs";
                    } else if (pageName.includes("logo")) {
                        pageName = "logos";
                    }
                    pageTitle.innerHTML = pageName;
                    pageTitle.style.display = 'inline';
                    mobileMenu.children[0].style.backgroundColor = 'var(--background-color)';
                    mobileMenu.children[1].style.backgroundColor = 'var(--background-color)';
                    mobileMenu.children[2].style.backgroundColor = 'var(--background-color)';
                } else {
                    pageTitle.style.display = 'none';
                }

                for (let menuItem of menuItems) {
                    menuItem.style.animation = 'none';
                    let id = menuItem.getAttribute('id').split('-')[0];
                    if (url.includes(id)) {
                        menuItem.nextElementSibling.style.width = "100%";
                        menuItem.nextElementSibling.style.borderTop = "3px solid var(--complementary-color)";
                    }
                }

                mobileMenu.style.animation = 'none';
                let logo = shadow.querySelectorAll(".logo")[0];
                logo.style.animation = 'none';

                
            }
        }
    }
    customElements.define('menu-component', MenuComponent);

}

In may html page I use it this way (this is the not-found page):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
    <script type="module" src="menu-component/menucomponent.js"></script>
    <title>Page not found</title>
</head>
<body>
    <menu-component></menu-component>
    <div class="content-container">
        <div class="page-title">
            Content not found
        </div>
        <div class="inner-container">
            <div class="text-description">
                It looks like there is a problem with the page you want to see. Please try with another content.</a>
            </div>
        </div>
    </div>
</body>
</html>

I noticed that I had a problem with styling the menu-component element: for an instant the element is displayed without style (no css applied) and then is rendered. I think this is the wel known problem of web component called FOUC. I followed advices and I added the following rules in my style.css:

menu-component:not(:defined) { 
    opacity: 0;
}

menu-component { 
    opacity: 1;
    transition: opacity 0.5s ease-in-out;
}

This looks like solving the problem, but not always. In chrome and firefox it's visible after a hard reload. In Safari it's always visible. How can I solve it?

I was expecting that the code in style.css resolved the problem.


Solution

  • <link> loads your CSS asynchronous, so the menu-component will get opacity:1 before all CSS is applied.

    Use the <link onload="..."> event to apply styles after the CSS file was loaded (and parsed)

    <my-component>Hello! Web Component</my-component>
    <script>
      customElements.define("my-component", class extends HTMLElement {
        constructor() {
          const element = (tag, props = {}) => Object.assign(document.createElement(tag), props);
          super()
            .attachShadow({mode:"open"})
            .append(
              this.STYLED = element("STYLE", { innerHTML: `:host{opacity:0}` }),
              element("STYLE", { innerHTML: `:host{display:inline-block}` }),
              element("LINK", {
                rel:   "stylesheet",
                href:  "https://cdn.sstatic.net/Sites/stackoverflow/primary.css",
                onload: (evt) => {
                  console.log("loaded CSS");
                  this.STYLED.innerHTML = `:host{transition:opacity 3s}`
                }
              }),
              element("SLOT"),
              element("P", {
                className: `is-required`,
                innerHTML: `P styled by stackoverflow/primary.css`
              })
            );
          console.log("created shadowDOM");
        }
        connectedCallback() {
          console.log("connectedCallback")
        }
      });
    </script>

    In your code both HTML and CSS could be fetched by One Web Component.
    You have given them the same name already.

    You can get the calling component with:

    <link rel="stylesheet" onload="console.warn(this.getRootNode().host)"
          href="menu-component/menu-component.css">
    

    But preferably let the Web Component handle loading CSS by injecting the <link> tag