Back to Fluentui

Using Fluent React with Web Components

apps/public-docsite-v9/src/Concepts/WebComponentsInterop/UsingFluentReactWithWebComponents.mdx

4.40.2-hotfix26.1 KB
Original Source

import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Concepts/Developer/Web Components Interop/Using Fluent React with Web Components" />

Using Fluent React with Web Components

Fluent React v9's extensible architecture and use of web platform features like CSS custom properties allow it to be extended to work well with Web Components, particularly shadow DOM.

It is necessary to modify Fluent React v9's default behavior to better interoperate with Web Components. The implementation lives in the Fluent UI Contrib repository.

Rendering Fluent React in shadow DOM

Fluent React components can be rendered inside shadow DOM with styles being set on the shadow root's adoptedStyleSheets property.

tsx
import { root } from '@fluentui-contrib/react-shadow';
import { FluentProvider, webLightTheme, Button } from '@fluentui/react-components';

<FluentProvider theme={webLightTheme}>
  <root.div>
    <Button>Fluent React Button in shadow DOM</Button>
  </root.div>
</FluentProvider>;

In the above example, note that FluentProvider sits outside the shadow DOM. When FluentProvider is inside the shadow DOM styling/theming will not work as expected.

⚠️ FluentProvider must be in the light DOM for this method to work.

tsx
// ❌ This will not render correctly, for example purposes only ❌
import { root } from '@fluentui-contrib/react-shadow';
import { FluentProvider, webLightTheme, Button } from '@fluentui/react-components';

/* This is the shadow root */
<root.div>
  <FluentProvider theme={webLightTheme}>
    <Button>Fluent React Button in shadow DOM</Button>
  </FluentProvider>
</root.div>;

To render a provider inside shadow DOM, use ThemelessFluentProvider instead.

ThemelessFluentProvider

ThemelessFluentProvider is a special version of FluentProvider that is designed to integrate with shadow DOM. It supports the same features as FluentProvider except:

  1. It does not insert styles into the DOM
  2. It removes the themeing related contexts

Because ThemelessFluentProvider provides no styles for the theme you must provide them yourself. This means creating a CSS rule that defines all the CSS custom properties needed for a Fluent theme. The theme can be created in React or another part of the application. The CSS custom properties just need to be defined in such a way that they will pierce the shadow DOM (e.g., on the :root selector).

tsx
import { createCSSStyleSheetFromTheme, ThemelessFluentProvider } from '@fluentui-contrib/react-themeless-provider';
import { root } from '@fluentui-contrib/react-shadow';
import { webLightTheme, Button } from '@fluentui/react-components';

// Create theme styles outside of React component rendering
const themeSheet = createCSSStyleSheetFromTheme(':root', webLightTheme);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, themeSheet];

// Render Fluent components
const ShadowDOMApp = () => {
  return (
    <ThemelessFluentProvider>
      <root.div>
        <Button>Fluent React Button in shadow DOM</Button>
      </root.div>
    </ThemelessFluentProvider>
  );
};

Keyboarding

Fluent React uses Tabster to control keyboarding and it must be able to "see" the entire DOM of a page to function correctly. Since shadow DOM "hides" parts of the DOM from Tabster, you must opt into shadow DOM support when using shadow DOM and Tabster together.

tsx
import { useShadowDOMSupport } from '@fluentui-contrib/pierce-dom';
import { root } from '@fluentui-contrib/react-shadow';
import { createCSSStyleSheetFromTheme, ThemelessFluentProvider } from '@fluentui-contrib/react-themeless-provider';
import { webLightTheme, Button } from '@fluentui/react-components';

const themeSheet = createCSSStyleSheetFromTheme(':root', webLightTheme);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, themeSheet];

const AppComponent = () => {
  // This must be called _before_ you render any Fluent React controls
  useShadowDOMSupport();
  <ThemelessFluentProvider>
    <root.div>
      <Button>First Button</Button>
      <Button>Second Button</Button>
    </root.div>
  </ThemelessFluentProvider>;
};

Insertion Point

Fluent uses Griffel for authoring styles and it provides the mergeClasses function for controlling style specificity. mergeClasses can accept any CSS class name but it only performs rule ordering for class names generated by Griffel. This can lead to CSS specificity issues when you want to use CSS classes that were not generated by Griffel (for example, application utility classes).

For those cases, use the insertion point API to control the specificity of styles via document order.

tsx
import { createRoot } from '@fluentui-contrib/react-shadow';
import { createCSSStyleSheetFromTheme, ThemelessFluentProvider } from '@fluentui-contrib/react-themeless-provider';
import { webLightTheme, Button } from '@fluentui/react-components';

const themeSheet = createCSSStyleSheetFromTheme(':root', webLightTheme);
document.head.adoptedStyleSheets = [...document.adoptedStyleSheets, themeSheet];

// This `CSSStyleSheet` acts as a sentinel for inserting Griffel styles.
// Griffel styles are inserted after `insertionPoint`.
const insertionPoint = new CSSStyleSheet();
const root = createRoot({ insertionPoint });

// These styles are not generated by Griffel.
const nonGriffelStyles = new CSSStyleSheet();
nonGriffelStyles.insertRule('.my-style-from-outside-fluent: { color: red; }');

// Griffel styles will be inserted after `insertionPoint` and
// before `nonGriffelStyles`, allowing styles defined in `nonGriffelStyles`
// to "win" specificity by appearing later in the document order.
const externalStyleSheets = [insertionPoint, nonGriffelStyles];

<ThemelessFluentProvider>
  <root.div styleSheets={externalStyleSheets}>
    <Button className="my-style-from-outside-fluent">Button with external style</Button>
  </root.div>
</ThemelessFluentProvider>;