Modern web development increasingly relies on web components to create reusable, encapsulated UI elements. However, a common challenge developers face is the "Flash of Unstyled Content" (FOUC) that occurs when custom elements appear on the page before their JavaScript definition loads. The CSS :defined pseudo-class provides an elegant solution to this problem, allowing developers to control how elements appear during their loading lifecycle.
Web components represent a fundamental shift in how we build UI elements, enabling true code reuse and encapsulation across projects. Whether you're building a custom button or a complex interactive widget, understanding how to handle loading states is critical for professional results.
What You'll Learn
- How the :defined pseudo-class works with web components
- Preventing visual disruptions during element loading
- Improving Cumulative Layout Shift (CLS) scores
- Best practices for production web components
- Browser compatibility and support
What is :defined?
The :defined CSS pseudo-class represents any element that has been defined. This includes two categories of elements:
Standard HTML elements built into the browser (such as <div>, <p>, <span>, etc.) are always considered defined because they are intrinsic to the browser. There is no registration process for these elements--they exist by default and are immediately available when used in HTML markup.
Custom elements start as undefined until their JavaScript definition is loaded and registered with the browser's CustomElementRegistry. This registration happens through the customElements.define() method, which associates a custom element name with a JavaScript class that defines its behavior. Understanding this registration process is essential for anyone working with web components in modern applications.
Here's how the registration process works:
// Define a custom element class that extends HTMLElement
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
button {
padding: 12px 24px;
background-color: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
<button>Click me</button>
`;
}
}
// Register the custom element with the browser
customElements.define('my-button', MyButton);
Before customElements.define() is called, any <my-button> elements in the HTML are considered undefined. Once this line executes, those elements transition to the defined state and will match the my-button:defined selector.
Key Distinction
/* Built-in elements are ALWAYS defined */
h1:defined { /* Always matches */ }
/* Custom elements are defined AFTER registration */
my-element:defined { /* Matches after customElements.define() */ }
Understanding this distinction is crucial for styling web components effectively throughout their loading lifecycle.
Syntax and Usage
The :defined pseudo-class offers flexibility in how you apply it to target elements. Understanding these different syntax patterns is essential for effective implementation.
Basic Syntax Patterns
/* Selects any defined element (both built-in and custom) */
:defined {
/* Styles for all defined elements */
}
/* Selects a specific custom element when it is defined */
my-element:defined {
display: block;
}
/* Selects a custom element when it is NOT defined */
my-element:not(:defined) {
visibility: hidden;
}
Universal and Type Selectors
The :defined pseudo-class can be combined with various selectors to achieve different targeting strategies:
/* Universal - applies to ALL defined elements on the page */
:defined {
box-sizing: border-box;
}
/* Type selector - targets a specific custom element by name */
custom-card:defined {
display: block;
contain: content;
}
/* Class-based - when custom elements have classes applied */
my-widget.fancy:defined {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
Practical Selector Patterns
/* Hide undefined custom elements completely from layout */
custom-element:not(:defined) {
display: none;
}
/* Show loading indicator for undefined elements */
custom-element:not(:defined)::before {
content: "Loading...";
display: block;
padding: 20px;
text-align: center;
background: #f5f5f5;
}
/* Apply smooth transitions after definition completes */
my-widget:defined {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
/* Combined with attribute selectors */
[data-loading]:not(:defined) {
position: relative;
}
These CSS techniques form the foundation of robust custom element handling in any web development project.
Why :defined is essential for web component development
Prevent FOUC
Eliminate flash of unstyled content by hiding elements until they are fully defined and ready to render.
Improve CLS Scores
Reduce Cumulative Layout Shift by controlling element visibility during the loading lifecycle.
Loading States
Display loading indicators or placeholders for custom elements while their JavaScript loads.
Native Solution
Standards-based CSS selector with excellent browser support since 2020.
Practical Use Cases
Preventing Flash of Unstyled Content
One of the most valuable applications of :defined is preventing FOUC--sometimes called "Flash of Undefined Content" (FOUI)--when working with web components. This visual disruption occurs when custom elements appear on the page before their JavaScript definition has loaded, leaving users momentarily with broken or partially styled interfaces.
The problem manifests in several ways:
- Unstyled markup: Users briefly see raw HTML tags or incomplete element structures
- Fallback content: Browser default styling appears before component styles apply
- Jarring transitions: Elements snap from hidden to visible without smooth transitions
The solution uses :not(:defined) to hide elements until ready:
/* Hide the custom element until it's fully defined */
my-element:not(:defined) {
visibility: hidden;
}
/* Reveal the element smoothly once defined */
my-element:defined {
visibility: visible;
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
to { opacity: 1; }
}
Improving Cumulative Layout Shift
Cumulative Layout Shift (CLS) measures visual stability during page loading. Undefined custom elements that suddenly appear can push content down, creating poor user experiences and negatively impacting SEO rankings. This is particularly important for search engine optimization, as Google uses Core Web Vitals as a ranking signal.
Consider this scenario without :defined:
- Page loads with a placeholder area for a custom card component
- The undefined element appears with minimal height
- JavaScript loads and the element expands to its full size
- Content below shifts downward abruptly
- User experience suffers and CLS score drops
Using :defined to reserve space prevents the shift:
custom-card:not(:defined) {
visibility: hidden;
min-height: 300px; /* Reserve expected space */
}
custom-card:defined {
visibility: visible;
}
Loading State Indicators
For components that require more than simple hiding, :defined enables sophisticated loading states:
custom-element:not(:defined)::before {
content: "Loading component...";
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background-color: #f0f0f0;
border-radius: 8px;
color: #666;
font-family: system-ui, sans-serif;
}
/* Add a subtle skeleton loader effect */
custom-element:not(:defined)::after {
content: "";
display: block;
width: 60%;
height: 4px;
background: linear-gradient(90deg, #e0e0e0, #f0f0f0, #e0e0e0);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
margin: 16px auto 0;
}
custom-element:defined::before,
custom-element:defined::after {
content: none;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Best Practices
Reserve Space vs. Hide Completely
When handling undefined elements, choose the approach that best fits your use case and user experience goals:
Hide Completely -- Use when the component is non-critical or its absence won't disrupt the layout:
my-element:not(:defined) {
display: none;
}
Use this approach for:
- Decorative elements that don't affect content understanding
- Components that load asynchronously without space constraints
- Progressive enhancement scenarios
Reserve Space -- Use when the component occupies known space in the layout:
my-element:not(:defined) {
visibility: hidden;
min-height: 200px;
}
Use this approach for:
- Main content components that affect page layout
- Cards, sections, or modules that push content below them
- Any element where sudden appearance would shift content
Combine with CSS Custom Properties
CSS custom properties (variables) work exceptionally well with :defined for creating dynamic, transition-ready styles:
my-element:not(:defined) {
--opacity: 0;
--transform: translateY(10px);
--loading-bg: #f5f5f5;
}
my-element:defined {
--opacity: 1;
--transform: translateY(0);
--loading-bg: transparent;
}
my-element {
opacity: var(--opacity);
transform: var(--transform);
transition: opacity 0.3s ease, transform 0.3s ease;
background: var(--loading-bg);
}
This approach keeps your CSS DRY and makes it easy to adjust animation timing across all components.
Performance Considerations
While :defined is a lightweight selector, consider these performance aspects for large-scale applications:
-
Use
display: noneon undefined elements to remove them from both layout and paint calculations entirely, improving initial page load performance -
Combine with
will-changefor animated properties to hint the browser about upcoming animations:
my-element:defined {
opacity: 1;
will-change: opacity, transform;
}
-
Batch selector updates by applying the same :not(:defined) pattern across multiple custom elements rather than writing individual rules for each
-
Consider load order -- placing custom element scripts in the
<head>withdeferor at the end of<body>can minimize the window where elements appear undefined
Browser Support
100%
Chrome Support
100%
Firefox Support
100%
Safari Support
2020
Available Since
Frequently Asked Questions
Conclusion
The :defined pseudo-class is an essential tool for web developers working with web components. By providing fine-grained control over how elements appear during their loading lifecycle, it helps:
- Prevent visual disruptions from FOUC that create jarring user experiences
- Improve layout stability and Core Web Vitals scores like CLS
- Enhance user experience with smooth transitions and loading indicators
- Create production-ready web components that feel polished and professional
With excellent browser support across all major browsers since 2020 and straightforward syntax, :defined should be a standard part of any web component development toolkit.
Key Takeaways
- :defined targets both built-in HTML elements and registered custom elements
- Use
:not(:defined)to handle undefined elements gracefully - Choose between hiding completely (display: none) or reserving space (visibility: hidden + min-height)
- Combine with CSS custom properties for smooth animations and transitions
- Essential for creating professional, production-quality web components
Building reliable web components requires attention to loading states, performance optimization, and user experience. Our web development services help teams implement best practices like these to create exceptional digital experiences.