Avoiding FOUC with Web Components

Flash of Unstyled Content (FOUC) happens when custom elements render before their definitions and styles are ready. This guide shows reliable, framework-friendly ways to avoid FOUC when using Web Components.

Core strategies (works everywhere)
  • Register components as early as possible: Import the web component bundle at module scope, not inside lifecycle hooks. This shortens the time elements are undefined.
  • Hide undefined custom elements: Use :not(:defined) to hide only elements that aren’t registered yet. They will appear as soon as the browser defines them.
  • Provide skeletons/placeholders: Use lightweight placeholders or the component’s built-in skeleton variant to avoid abrupt layout shifts.
  • Load component CSS early: If your design system ships separate CSS, ensure it’s imported globally as early as possible.

Example CSS guard:

/* Hide only custom elements until defined */
:not(:defined) {
  visibility: hidden;
}

Example early registration (vanilla):

<script type="module">
  import 'dap-design-system'
</script>
Next.js / React specifics
  • Prefer module-scope imports in a client module that loads early. Avoid deferring to useEffect, which runs after paint.
  • Keep the :not(:defined) rule in a global stylesheet loaded via app/layout. This prevents paint until elements are defined.
  • Optional: Preload the design system bundle and CSS if they are large.

Minimal Next.js setup:

// app/clientApplication.tsx
'use client'
import { ReactNode, useEffect } from 'react'

export default function ClientApplication({
  children,
}: {
  children: ReactNode
}) {
  useEffect(() => {
    async function getComponents() {
      await import('dap-design-system')
      await import('dap-design-system/react')
    }

    getComponents()
  }, [])

  return children
}
// app/app.scss
@import url('dap-design-system/styles/light.theme.css');
@import url('dap-design-system/styles/components.native.css');
@import url('dap-design-system/styles/dds-reset.css');

:not(:defined) {
  visibility: hidden;
}

Other React notes:

  • If you server-render HTML for faster TTFB, ensure the client bundle that defines custom elements loads immediately after HTML to minimize hidden time.
  • Avoid conditionally rendering custom elements only after mount; that trades FOUC for layout shifts.
Angular specifics
  • Enable custom elements: add CUSTOM_ELEMENTS_SCHEMA to your module or use standalone components with the same schema.
  • Register Web Components in a top-level, eagerly loaded file (e.g., main.ts), not inside a component lifecycle hook.
  • Add the :not(:defined) rule in styles.scss.
// main.ts
import 'dap-design-system'
// bootstrapApplication(AppComponent)
/* styles.scss */
:not(:defined) {
  visibility: hidden;
}
Vue 2/3 specifics
  • Import the web component bundle in your main entry (e.g., main.js/main.ts).
  • Avoid lazy-registering inside components; prefer app-level import.
  • Put :not(:defined) in a global CSS file (e.g., src/assets/main.css), not in a scoped style block.
// main.ts
import 'dap-design-system'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
Svelte / SvelteKit specifics
  • Import the web component bundle in a top-level client entry (+layout.svelte or a client hook) so components are defined before initial paint.
  • Add the CSS guard globally (e.g., app.css).
&lt;!-- +layout.svelte --&gt;
&lt;script&gt;
  import &#39;dap-design-system&#39;
&lt;/script&gt;
&lt;slot /&gt;
Advanced optimizations (optional)
  • Early <script type="module"> in <head>: Inline a tiny module import in the document head to start parsing the design system before the body.
  • Preload hints: Use <link rel="modulepreload" href="/path/to/ds.js"> and <link rel="preload" as="style" href="/path/to/ds.css"> to bring assets in sooner.
  • SSR-friendly placeholders: If components render complex content, server-render a minimal placeholder that matches final dimensions to avoid CLS.
Common pitfalls
  • Importing the design system only inside mount hooks (useEffect, mounted, ngOnInit) — too late, causes FOUC.
  • Scoping the :not(:defined) rule inside component-scoped CSS — it won’t affect global elements.
  • Hiding entire body — avoids FOUC but hurts perceived performance and can harm accessibility.
Quick checklist
  • Import the web component bundle at the earliest possible point (module scope, app entry).
  • Add the :not(:defined) visibility guard globally.
  • Ensure design system CSS is loaded early.
  • Prefer skeletons/placeholders over delayed rendering.

With these practices, FOUC is either eliminated or reduced to a non-perceptible transient while keeping the page accessible and fast.