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.
- 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>
- 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 viaapp/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.
- Enable custom elements: add
CUSTOM_ELEMENTS_SCHEMAto 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 instyles.scss.
// main.ts
import 'dap-design-system'
// bootstrapApplication(AppComponent)
/* styles.scss */
:not(:defined) {
visibility: hidden;
}
- 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')
- Import the web component bundle in a top-level client entry (
+layout.svelteor a client hook) so components are defined before initial paint. - Add the CSS guard globally (e.g.,
app.css).
<!-- +layout.svelte -->
<script>
import 'dap-design-system'
</script>
<slot />- 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.
- 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.
- 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.