Creating any part of your page using scripting can lead to an effect where the effects of the scripting appear to pop in as the page loads. This, like a flash of unstyled content, never looks great, and in the worst case it can cause a layout shift, which is pretty jarring.

One way around this is to not build the layout of your page with scripting. That's great if you can swing it - nothing's faster than rendering pre-built HTML. That can mean using server-side rendering or a static site generator (like 11ty); I recommend these things, they're pretty good.

However, there are cases where client-side rendering make a lot of sense. I use a lot of pretty interactive web components and don't like writing anything server-side for my hobby projects (which is just a matter of personal taste), so I end up doing a fair bit of client-side rendering. For example, I maintain a consistent header on this site using a custom element: <gwc-header>. It suits neocities really well, but it renders the entire header from nothing once its been sited, and pulls in a couple other web components itself (my personalization and search controls), so it can take a moment to get its elements laid down. This results in the entire page shifting down as the header loads in.

To avoid this, I made a little snippit of code I'm calling a Veil. It prevents the browser from showing anything until the DOMConentLoaded event fires (and a 0s timeout to ensure a full callstack has unwound). That's enough time for most basic client-side rendering, but my header has to load a couple scripts for the search and personalization controls, so it needs a bit longer. To accomodate this, the Veil allows scripting to extend how long it is shown.

The Veil is just a bit of CSS that makes the <body> element invisible and sets a background color on the <html> element based on the perfers-color-scheme media feature. Once the page has loaded and any scripts confirm they're done, the Veil's CSS is removed.

In order to make sure the Veil goes up as soon as the page is parsed, it needs to be an inline script, duplicated across every page. That's always risky since there's a high cost to fix bugs, so I made it as simple as I could. Here's what it looks like:

<script>
  /**
  * A script for a loading veil
  * @author Vera Konigin vera@groundedwren.com
  * https://groundedwren.neocities.org
  */
  window.GW = window.GW || {};
  window.GW.Gizmos = window.GW.Gizmos || {};
  (function Veil(ns) {
    ns.DeferKeys = {"DOMContentLoaded": true};
  
    /**
     * Defers the lowering of the veil until a key is cleared
     * @param {string} key The defer key to keep the veil up
     */
    ns.addDefer = function addDefer(key) {
      ns.DeferKeys[key] = true;
    }
  
    /**
     * Marks a defer key as complete and clears the veil if no remain
     * @param {string} key The key to clear
     */
    ns.clearDefer = function clearDefer(key) {
      delete ns.DeferKeys[key];
      if(Object.keys(ns.DeferKeys).length === 0) {
        setTimeout(() => document.getElementById("styGwLoad")?.remove(), 0);
      }
    }
  
    document.head.insertAdjacentHTML("beforeend", `
      <style id="styGwLoad">
        body {
          opacity: 0;
        }
        html {
          width: 100vw;
          height: 100vh;
          background-color: var(--veil-light-color, #E3E3E3) !important;
        }
        @media(prefers-color-scheme: dark) {
          html {
            background-color: var(--veil-dark-color, #242424) !important;
          }
        }
      </style>
    `);
  
    window.addEventListener("DOMContentLoaded", function onDOMContentLoadedVeil() {
      ns.clearDefer("DOMContentLoaded");
    });
  }) (window.GW.Gizmos.Veil = window.GW.Gizmos.Veil || {});
</script>

The header (or any scripting) can extend the Veil by calling GW.Gizmos?.Veil?.addDefer("<key>");, and then say it's ready by calling GW.Gizmos?.Veil?.clearDefer("<key>");.