The Blueprint: Researching PCF Best Practices Before Coding
Every PCF developer knows the urge: run pac pcf init and start coding. One developer building a Fluent UI v9 component for a Model-driven app resisted it, and that choice avoided a problem that usually shows up only after deployment: CSS that breaks in unpredictable ways.
The requirement looked routine: a highly interactive, custom UI component for a Dynamics 365 Model-driven app, built with the Power Apps Component Framework (PCF) and the latest Fluent UI v9. Instead of coding right away, the developer researched enterprise best practices across community forums, GitHub issues and architectural blogs.
One concept kept coming up: The Shadow DOM. This blog explains what it is, why it protects PCF controls from CSS conflicts, and how to make it work with Fluent UI v9, with working code.
Key Takeaways
- Shadow DOM isolates a PCF control’s styles from Dynamics 365’s global CSS, in both directions.
- Without it, global styles can distort the control, and the control’s styles can break the native UI.
- Fluent UI v9 injects styles into the document <head>, so components inside a Shadow Root render unstyled.
- Using createDOMRenderer(shadowRoot) sends Fluent UI styles into the shadow root and fixes this.
- Shadow DOM adds almost no performance overhead, and mode: ‘open’ keeps debugging easy.
What is the Shadow DOM?
Shadow DOM is a standard Web Components API that allows you to attach a hidden, separated DOM to an element. Think of it as an ‘invisible wall’ or a quarantine zone within your HTML.
To understand why this “invisible wall” is so crucial for PCF controls, we need to look at the exact problem it solves.
The Problem: CSS Bleeding in Power Apps
Model-driven apps are essentially massive Single-Page Applications (SPAs) loaded with hundreds of thousands of lines of global CSS.
When you deploy a PCF control, by default, it doesn’t run in an isolated iframe (which is good for performance). Instead, it gets rendered directly into the exact same Document Object Model (DOM) as the rest of the CRM form. This lack of boundaries leads to two main issues:
- Inward Bleeding: The global Dynamics 365 CSS rules target generic HTML tags (like <button>, <table>, or <div>). Without protection, these global styles cascade down into your PCF control, distorting your margins, fonts, and layouts.
- Outward Bleeding: If your PCF control (or a third-party library you use) injects its own global CSS, it can leak out and accidentally override the native Dynamics 365 ribbons and form fields.
Often, developers try to fix this by using specific CSS class names or adding the important tag to their styles. However, this approach becomes very difficult to maintain over time.
By placing your PCF control’s HTML inside a Shadow Root, you completely eliminate this problem. Styles defined inside cannot leak out, and global CRM styles cannot penetrate in. It was exactly what I needed, but there was a catch.
The Challenge: PCF, Shadow DOM, and Fluent UI v9
Implementing Shadow DOM in vanilla HTML and JavaScript is simple. This project, however, used a React-based PCF control with Fluent UI v9, which added an architectural challenge.
Fluent UI v9 uses a highly optimized CSS-in-JS engine. By default, it generates CSS classes dynamically and injects them into the <head> of the main document. Components living inside a Shadow DOM are immune to styles injected there, so Fluent UI components would render with no styling at all.
The fix is to configure the Fluent UI v9 render er to inject its stylesheets into the Shadow Root instead of the document <head>. The code below shows how to achieve this isolation from day one.
Step-by-Step Implementation Guide
Step 1: Initializing Shadow DOM in index.ts
In the PCF control’s index.ts file, we need to create a container element, attach a shadow root to it, and pass that root to our React rendering logic.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from "./App";
export class IsolatedPCFControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _container: HTMLDivElement;
private _shadowRoot: ShadowRoot;
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this._container = container;
// 1. Create a wrapper div
const shadowWrapper = document.createElement("div");
shadowWrapper.style.width = "100%";
shadowWrapper.style.height = "100%";
// 2. Attach the Shadow DOM (mode: open allows us to inspect it in DevTools)
this._shadowRoot = shadowWrapper.attachShadow({ mode: "open" });
// 3. Append the wrapper to the main PCF container
this._container.appendChild(shadowWrapper);
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
// Render the React app INSIDE the shadow root
ReactDOM.render(
React.createElement(App, {
context: context,
shadowRoot: this._shadowRoot // Pass shadowRoot as a prop!
}),
this._shadowRoot as any
);
}
public destroy(): void {
ReactDOM.unmountComponentAtNode(this._shadowRoot as any);
}
}
Step 2: Configuring the Fluent UI v9 Renderer in React
Now, inside the App.tsx component, we receive the shadowRoot. We must create a custom DOM render er that tells Fluent UI to insert its CSS directly into this shadow node.
import * as React from "react";
import {
FluentProvider,
webLightTheme,
createDOMRenderer,
RendererProvider,
Button
} from "@fluentui/react-components";
export interface IAppProps {
context: ComponentFramework.Context<any>;
shadowRoot: ShadowRoot;
}
export const App: React.FC<IAppProps> = ({ context, shadowRoot }) => {
// Create a renderer that targets our Shadow DOM instead of document.head
const renderer = React.useMemo(() => {
return createDOMRenderer(shadowRoot);
}, [shadowRoot]);
return (
// Provide the custom renderer to the Fluent UI tree
<RendererProvider renderer={renderer}>
<FluentProvider theme={webLightTheme}>
<div style={{ padding: '20px' }}>
<h2>Isolated PCF Control</h2>
<p>This UI is completely protected from D365 CSS Bleeding.</p>
<Button appearance="primary">Click Me</Button>
</div>
</FluentProvider>
</RendererProvider>
);
};
Performance and Debugging
Is this heavy? Not at all. Unlike the legacy approach of loading PCF controls inside an IFRAME (which initializes a whole new browser context and consumes significant memory), Shadow DOM is incredibly lightweight. The browser engine handles it natively with virtually zero performance overhead.
To debug, simply press F12, open the Elements tab, and inspect your PCF container. You will see a #shadow-root (open) node. Expand it, and you’ll beautifully see both your HTML elements and the <style> tags injected specifically by Fluent UI v9 just for this control.
Conclusion: Planning Pays Off
Researching before coding spared this project a major deployment headache. As Model-driven apps evolve and Microsoft updates its base UI, fragile CSS overrides are a recipe for maintenance problems.
Best Practice Summary for Enterprise PCF:
- Always implement Shadow DOM for complex PCF controls, especially those using external UI libraries.
- Remember to pass the ShadowRoot reference down to your styling engine (whether it’s Fluent UI v9, Material UI, or styled-components).
- Set mode: ‘open’ during development to allow easy debugging in browser dev tools.
Adopting this architecture ensures your custom controls remain robust, completely isolated, and visually perfect, no matter what changes occur in the surrounding Dynamics 365 environment.
