# Styling with CSS Modules

Use RendererProvider to scope Griffel/Fluent styles into a CSS cascade `@layer`s so plain CSS/CSS Modules can override CoreAI component styles.

Fluent UI generates component styles with
[Griffel](https://github.com/microsoft/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:

```css
/* 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:

```sh
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`:

```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`](https://github.com/microsoft/coreai-design-system/blob/main/apps/prototype/app/theme/createLayeredRenderer.ts).

### 3. Supply the renderer above the theme provider

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

```tsx
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](https://griffel.js.org/react/api/create-dom-renderer/).

### 4. Declare the layer order

Load this declaration before component styles:

```css
@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

```css
/* Button.module.css */
.cta {
  border-radius: var(--borderRadiusLarge);
}
```

```tsx
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](https://griffel.js.org/react/guides/ssr-usage/).

## 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.