Fluent UI generates component styles with Griffel and inserts CSS rules at runtime. An app that uses plain CSS or CSS Modules can opt into an app-owned renderer that wraps those rules in a CSS cascade layer named griffel.

Normal, unlayered CSS then outranks normal rules in that layer, regardless of selector specificity or insertion order. This allows a CSS Module class to override Fluent layout or appearance without !important.

Use RendererProvider, not a dependency patch. The renderer applies only to its React subtree. Do not edit node_modules, install a patch-package hook, or globally replace Griffel for other workspaces.

Opt-in: @coreai-ds/react-components does not enable layering automatically. The prototype uses this method. The docsite, Storybook, and package consumers retain their default renderers unless they explicitly opt in.

Why cascade layers

For normal declarations, unlayered author styles take priority over layered author styles:

/* Fluent / Griffel styles — high specificity, but layered = lowest priority */
@layer griffel {
  .f1w7gpdv {
    /* .fui-Link */
    display: block;
  }
}

/* Your CSS Module — a single class, unlayered = always wins */
.navLink {
  display: flex;
  align-items: center;
}

Here .navLink wins even if the Griffel selector is more specific or its rule is inserted later. This is a browser cascade rule, not a change to mergeClasses. Griffel class merging still resolves conflicts before the browser sees the CSS.

!important reverses layer priority. Do not treat "unlayered wins" as a guarantee for important declarations.

Set up an app-owned renderer

1. Add the renderer dependency

Declare @griffel/react as a direct dependency of the consuming app. Choose a version compatible with its Fluent dependencies:

npm install @griffel/react

In a monorepo, install it in the app workspace. Keep the provider and Fluent components on the same Griffel React instance; duplicate installations can create separate contexts.

2. Create the layered renderer

Copy this into an app-owned createLayeredRenderer.ts:

import { createDOMRenderer, type GriffelRenderer } from '@griffel/react';

type CSSRulesByBucket = Parameters<GriffelRenderer['insertCSSRules']>[0];

export function createLayeredRenderer(targetDocument?: Document): GriffelRenderer {
  const renderer = createDOMRenderer(targetDocument);
  const insertCSSRules = renderer.insertCSSRules.bind(renderer);

  renderer.insertCSSRules = (rules) => {
    const layeredRules: CSSRulesByBucket = {};

    for (const bucket of Object.keys(rules) as (keyof CSSRulesByBucket)[]) {
      layeredRules[bucket] = rules[bucket]?.map((entry) => {
        // Tuple metadata controls Griffel's media/container stylesheet ordering.
        if (typeof entry === 'string') {
          return `@layer griffel{${entry}}`;
        }
        return [`@layer griffel{${entry[0]}}`, entry[1]];
      });
    }

    insertCSSRules(layeredRules);
  };

  return renderer;
}

The wrapper changes rule text but retains rule buckets and tuple metadata. The original renderer still manages stylesheets, query ordering, and insertion deduplication. Do not flatten the buckets or drop media/container metadata.

The prototype's implementation is createLayeredRenderer.ts.

3. Supply the renderer above the theme provider

Create the renderer once per provider instance, not on every render:

import { useState, type PropsWithChildren } from 'react';
import { RendererProvider } from '@griffel/react';
import { CoreAIFluentProvider, coreaiLightTheme } from '@coreai-ds/react-components';
import { createLayeredRenderer } from './createLayeredRenderer';

export function AppProvider({ children }: PropsWithChildren) {
  const [renderer] = useState(() => createLayeredRenderer());

  return (
    <RendererProvider renderer={renderer}>
      <CoreAIFluentProvider theme={coreaiLightTheme}>{children}</CoreAIFluentProvider>
    </RendererProvider>
  );
}

The provider selects the renderer through React context. Components outside this subtree use their usual renderer. Portals retain React context, so dialogs and popovers inside the subtree also use the layered renderer.

For child windows or iframes, create the renderer for the target document and pass that same document to RendererProvider. See Griffel's createDOMRenderer documentation.

4. Declare the layer order

Load this declaration before component styles:

@layer griffel, common;

The common layer is optional. Use it for app-owned shared base styles if your app needs that intermediate priority. Leave page-specific CSS Modules unlayered. Declaring the layers alone does not wrap Griffel's rules; the renderer does that.

Use CSS Modules normally

/* Button.module.css */
.cta {
  border-radius: var(--borderRadiusLarge);
}
import { Button } from '@coreai-ds/react-components';
import styles from './Button.module.css';

export function Cta() {
  return <Button className={styles.cta}>Continue</Button>;
}

Prefer documented component props and theme tokens when they express the desired behavior. Layering is useful for app-owned CSS overrides; it does not replace CoreAI's component defaults or the correct mergeClasses order.

Production builds and CSS extraction

A normal production build does not bypass this renderer. Runtime-inserted rules still pass through it in the browser. Precomputing Griffel style definitions also remains compatible when the resulting code still inserts rules at runtime.

Static CSS extraction is different. Extracted CSS files load directly in the browser and bypass the renderer. If you adopt extraction, configure the build to wrap that generated CSS in @layer griffel too. The former dependency patch had the same limitation.

For SSR, use a request-owned renderer and Griffel's style collection/rehydration setup. Do not share a mutable renderer between requests. See Griffel's SSR guide.

Limits and upgrade checks

  • Public provider, internal insertion method. RendererProvider and createDOMRenderer are public APIs. Griffel marks insertCSSRules as private. This adapter is not a documented layer option. Revalidate it after upgrading Griffel or Fluent.
  • Scoped, not global. A nested renderer provider can replace this one. Components using another Griffel context do not inherit it.
  • Only renderer-managed rules. Theme-variable style tags and unrelated stylesheets are not automatically layered.
  • Small extra work. The adapter maps rule arrays and wraps strings before cache checks. No performance benchmark is implied.
  • Keep base styles scoped. Unlayered global rules can now override normal component styles unintentionally.
  • Browser support. The app must target browsers that support CSS cascade layers.

After an upgrade, inspect the rules under style[data-make-styles-bucket] and confirm they are inside @layer griffel. Check a real CSS Module override in development and production preview, including query styles, focus/hover states, portals, and navigation. In the prototype, the top-navigation brand link should compute to display: flex and the neutral foreground color, with its logo and text horizontally aligned at desktop width.

Migrating from the dependency patch

Add and validate the app-owned renderer first. Then remove the old Griffel patch, its version pin if used only for that patch, and its installation hook if no other patches need it. Restore an unpatched dependency installation before validation. Do not run both approaches: that can wrap rules twice and keeps the unwanted shared-installation behavior.