Repository: microsoft/griffel
Pull request #139 (2022-06-16 14:46:07)

feat: add Webpack plugin for CSS extraction
This PR adds a Webpack loader that invokes Babel plugin (#135) and performs CSS extraction.

P.S. The next PR will contain a Webpack plugin to concatenate and deduplicate rules.

github-actions[bot]: ## 📊 Bundle size report

<details>
<summary>Unchanged fixtures</summary>

| Package & Exports                                                                                                            |        Size (minified/GZIP) |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------: |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>       |       `1.8 kB`<br />`861 B` |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStylesRuntime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr>   | `20.269 kB`<br />`7.549 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStaticStyles-runtime.fixture.js'>makeStaticStyles (runtime)</abbr>    |  `8.247 kB`<br />`3.595 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles-runtime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr> |  `21.294 kB`<br />`7.98 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>      |  `2.811 kB`<br />`1.275 kB` |

</details>
<sub>🤖 This report was generated against <a href='https://github.com/microsoft/griffel/commit/7bf674dd9dd896fdfe8346e6a33c8866dbe78bf3'>7bf674dd9dd896fdfe8346e6a33c8866dbe78bf3</a></sub>

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/apps/benchmark/src/App.tsx b/apps/benchmark/src/App.tsx
index 6eb05d6..a1d192d 100644
--- a/apps/benchmark/src/App.tsx
+++ b/apps/benchmark/src/App.tsx
@@ -22,7 +22,7 @@ interface AppState {
 
 const appInitialState: AppState = {
   benchmark: 'Mount deep tree',
-  library: Object.keys(implementations)[0],
+  library: 'griffel',
   results: [],
   inProgress: false,
 };
diff --git a/apps/website/src/components/Playground/code/app.js b/apps/website/src/components/Playground/code/app.js
index fcd523d..12edae0 100644
--- a/apps/website/src/components/Playground/code/app.js
+++ b/apps/website/src/components/Playground/code/app.js
@@ -23,7 +23,7 @@ export default function App() {
         setRules(prettified);
       },
       insertionCache: {},
-      styleElements: {},
+      stylesheets: {},
     };
 
     styles({
diff --git a/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json b/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json
new file mode 100644
index 0000000..78f6128
--- /dev/null
+++ b/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Create isomorphic stylesheet for DOM renderer",
+  "packageName": "@griffel/core",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-b0216f7b-4524-4ecc-ae6b-a2d7f1493caf.json b/change/@griffel-core-b0216f7b-4524-4ecc-ae6b-a2d7f1493caf.json
new file mode 100644
index 0000000..7ec46bc
--- /dev/null
+++ b/change/@griffel-core-b0216f7b-4524-4ecc-ae6b-a2d7f1493caf.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Insert media queries into separate buckets based on the media query value",
+  "packageName": "@griffel/core",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json b/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json
new file mode 100644
index 0000000..021a7a3
--- /dev/null
+++ b/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Update renderToStyleElement to use isomorphic stylesheets from renderer",
+  "packageName": "@griffel/react",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-dcbfe547-31b2-46aa-891f-5de203376148.json b/change/@griffel-react-dcbfe547-31b2-46aa-891f-5de203376148.json
new file mode 100644
index 0000000..ce8b782
--- /dev/null
+++ b/change/@griffel-react-dcbfe547-31b2-46aa-891f-5de203376148.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: Rename instances of styleElement to stylesheet where approriate following [#137](https://github.com/microsoft/griffel/pull/137)",
+  "packageName": "@griffel/react",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/babel-preset/__fixtures__/rules-with-metadata/code.ts b/packages/babel-preset/__fixtures__/rules-with-metadata/code.ts
new file mode 100644
index 0000000..0e4b2e6
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/rules-with-metadata/code.ts
@@ -0,0 +1,9 @@
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  media: {
+    '@media screen and (max-width: 100px)': {
+      color: 'red',
+    },
+  },
+});
diff --git a/packages/babel-preset/__fixtures__/rules-with-metadata/output.ts b/packages/babel-preset/__fixtures__/rules-with-metadata/output.ts
new file mode 100644
index 0000000..f4541c5
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/rules-with-metadata/output.ts
@@ -0,0 +1,18 @@
+import { __styles } from '@griffel/react';
+export const useStyles = __styles(
+  {
+    media: {
+      Bulngiv: 'fr5o61b',
+    },
+  },
+  {
+    m: [
+      [
+        '@media screen and (max-width: 100px){.fr5o61b{color:red;}}',
+        {
+          m: 'screen and (max-width: 100px)',
+        },
+      ],
+    ],
+  },
+);
diff --git a/packages/core/src/common/snapshotSerializers.ts b/packages/core/src/common/snapshotSerializers.ts
index c2ec844..156e2e1 100644
--- a/packages/core/src/common/snapshotSerializers.ts
+++ b/packages/core/src/common/snapshotSerializers.ts
@@ -3,6 +3,7 @@
 import * as prettier from 'prettier';
 
 import { resolveStyleRules } from '../runtime/resolveStyleRules';
+import { normalizeCSSBucketEntry } from '../runtime/utils/normalizeCSSBucketEntry';
 import type { GriffelRenderer } from '../types';
 
 // eslint-disable-next-line eqeqeq
@@ -10,7 +11,7 @@ const isObject = (value: unknown) => value != null && !Array.isArray(value) && t
 
 export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
   test(value) {
-    return isObject(value) && isObject(value.styleElements);
+    return isObject(value) && isObject(value.stylesheets);
   },
   print(value) {
     /**
@@ -18,15 +19,15 @@ export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
      */
     const _value = value as GriffelRenderer;
 
-    const styleElementsKeys = Object.keys(_value.styleElements) as (keyof typeof _value['styleElements'])[];
+    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof typeof _value['stylesheets'])[];
 
-    const rules = styleElementsKeys.reduce((acc, styleEl) => {
-      const styleElement = _value.styleElements[styleEl];
+    const rules = stylesheetKeys.reduce((acc, styleEl) => {
+      const stylesheet = _value.stylesheets[styleEl];
 
-      if (styleElement) {
-        const cssRules = styleElement.sheet ? Array.from(styleElement.sheet.cssRules) : [];
+      if (stylesheet) {
+        const cssRules = stylesheet.cssRules() ?? ([] as string[]);
 
-        return [...acc, ...cssRules.map(rule => rule.cssText)];
+        return [...acc, ...cssRules];
       }
 
       return acc;
@@ -53,7 +54,7 @@ export const griffelRulesSerializer: jest.SnapshotSerializerPlugin = {
     const keys = Object.keys(cssRulesByBucket) as (keyof typeof cssRulesByBucket)[];
 
     return keys.reduce((acc, styleBucketName) => {
-      const rules = cssRulesByBucket[styleBucketName]!;
+      const rules = cssRulesByBucket[styleBucketName]!.map(entry => normalizeCSSBucketEntry(entry)[0]);
 
       return prettier.format(acc + rules.join(''), { parser: 'css' }).trim();
     }, '');
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
index 2bfd62a..4c49715 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -1,6 +1,9 @@
 import * as CSS from 'csstype';
 import { LookupItem, SequenceHash } from './types';
 
+/** @internal */
+export const DATA_BUCKET_ATTR = 'data-make-styles-bucket';
+
 /** @internal */
 export const HASH_PREFIX = 'f';
 
diff --git a/packages/core/src/renderer/createDOMRenderer.ts b/packages/core/src/renderer/createDOMRenderer.ts
index 0f36b4d..cbae030 100644
--- a/packages/core/src/renderer/createDOMRenderer.ts
+++ b/packages/core/src/renderer/createDOMRenderer.ts
@@ -1,4 +1,5 @@
 import { injectDevTools, isDevToolsEnabled, debugData } from '../devtools';
+import { normalizeCSSBucketEntry } from '../runtime/utils/normalizeCSSBucketEntry';
 import { GriffelRenderer, StyleBucketName } from '../types';
 import { getStyleSheetForBucket } from './getStyleSheetForBucket';
 
@@ -35,7 +36,7 @@ export function createDOMRenderer(
   const { unstable_filterCSSRule } = options;
   const renderer: GriffelRenderer = {
     insertionCache: {},
-    styleElements: {},
+    stylesheets: {},
 
     id: `d${lastIndex++}`,
 
@@ -43,13 +44,17 @@ export function createDOMRenderer(
       // eslint-disable-next-line guard-for-in
       for (const styleBucketName in cssRules) {
         const cssRulesForBucket = cssRules[styleBucketName as StyleBucketName]!;
-        const sheet =
-          target &&
-          getStyleSheetForBucket(styleBucketName as StyleBucketName, target, renderer, options.styleElementAttributes);
 
         // This is a hot path in rendering styles: ".length" is cached in "l" var to avoid accesses the property
         for (let i = 0, l = cssRulesForBucket.length; i < l; i++) {
-          const ruleCSS = cssRulesForBucket[i];
+          const [ruleCSS, metadata] = normalizeCSSBucketEntry(cssRulesForBucket[i]);
+          const sheet = getStyleSheetForBucket(
+            styleBucketName as StyleBucketName,
+            target,
+            renderer,
+            options.styleElementAttributes,
+            metadata,
+          );
 
           if (renderer.insertionCache[ruleCSS]) {
             continue;
@@ -60,21 +65,19 @@ export function createDOMRenderer(
             debugData.addCSSRule(ruleCSS);
           }
 
-          if (sheet) {
-            try {
-              if (unstable_filterCSSRule) {
-                if (unstable_filterCSSRule(ruleCSS)) {
-                  sheet.insertRule(ruleCSS, sheet.cssRules.length);
-                }
-              } else {
-                sheet.insertRule(ruleCSS, sheet.cssRules.length);
-              }
-            } catch (e) {
-              // We've disabled these warnings due to false-positive errors with browser prefixes
-              if (process.env.NODE_ENV !== 'production' && !ignoreSuffixesRegex.test(ruleCSS)) {
-                // eslint-disable-next-line no-console
-                console.error(`There was a problem inserting the following rule: "${ruleCSS}"`, e);
+          try {
+            if (unstable_filterCSSRule) {
+              if (unstable_filterCSSRule(ruleCSS)) {
+                sheet.insertRule(ruleCSS);
               }
+            } else {
+              sheet.insertRule(ruleCSS);
+            }
+          } catch (e) {
+            // We've disabled these warnings due to false-positive errors with browser prefixes
+            if (process.env.NODE_ENV !== 'production' && !ignoreSuffixesRegex.test(ruleCSS)) {
+              // eslint-disable-next-line no-console
+              console.error(`There was a problem inserting the following rule: "${ruleCSS}"`, e);
             }
           }
         }
diff --git a/packages/core/src/renderer/createIsomorphicStyleSheet.ts b/packages/core/src/renderer/createIsomorphicStyleSheet.ts
new file mode 100644
index 0000000..56860cf
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleSheet.ts
@@ -0,0 +1,53 @@
+import { DATA_BUCKET_ATTR } from '../constants';
+import { IsomorphicStyleSheet, StyleBucketName } from '../types';
+
+export function createIsomorphicStyleSheet(
+  styleElement: HTMLStyleElement | undefined,
+  bucketName: StyleBucketName,
+  elementAttributes: Record<string, string>,
+): IsomorphicStyleSheet {
+  // no CSSStyleSheet in SSR, just append rules here for server render
+  const __cssRulesForSSR: string[] = [];
+
+  elementAttributes[DATA_BUCKET_ATTR] = bucketName;
+  if (styleElement) {
+    for (const attrName in elementAttributes) {
+      styleElement.setAttribute(attrName, elementAttributes[attrName]);
+    }
+  }
+
+  function insertRule(rule: string) {
+    if (styleElement?.sheet) {
+      return styleElement.sheet.insertRule(rule, styleElement.sheet.cssRules.length);
+    }
+
+    return __cssRulesForSSR.push(rule);
+  }
+
+  return {
+    elementAttributes,
+    insertRule,
+    element: styleElement,
+    bucketName,
+    cssRules() {
+      if (styleElement?.sheet) {
+        return Array.from(styleElement.sheet.cssRules).map(cssRule => cssRule.cssText);
+      }
+
+      return __cssRulesForSSR;
+    },
+  };
+}
+
+export function createIsomorphicStyleSheetFromElement(element: HTMLStyleElement) {
+  const elementAttributes = Array.from(element.attributes).reduce((acc, attr) => {
+    acc[attr.name] = attr.value;
+    return acc;
+  }, {} as Record<string, string>);
+  const stylesheet = createIsomorphicStyleSheet(
+    element,
+    elementAttributes[DATA_BUCKET_ATTR] as StyleBucketName,
+    elementAttributes,
+  );
+  return stylesheet;
+}
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 9e59742..de8134e 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -1,4 +1,5 @@
-import { GriffelRenderer, StyleBucketName } from '../types';
+import { GriffelRenderer, IsomorphicStyleSheet, StyleBucketName } from '../types';
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
 
 /**
  * Ordered style buckets using their short pseudo name.
@@ -26,6 +27,8 @@ export const styleBucketOrdering: StyleBucketName[] = [
   'k',
   // at-rules
   't',
+  // @media rules
+  'm',
 ];
 
 /**
@@ -33,34 +36,43 @@ export const styleBucketOrdering: StyleBucketName[] = [
  */
 export function getStyleSheetForBucket(
   bucketName: StyleBucketName,
-  target: Document,
+  target: Document | undefined,
   renderer: GriffelRenderer,
   elementAttributes: Record<string, string> = {},
-): CSSStyleSheet {
-  if (!renderer.styleElements[bucketName]) {
-    let currentBucketIndex = styleBucketOrdering.indexOf(bucketName) + 1;
-    let nextBucketFromCache = null;
+  metadata?: Record<string, unknown>,
+): IsomorphicStyleSheet {
+  let stylesheetKey: StyleBucketName | string = bucketName;
 
-    // Find the next bucket which we will add our new style bucket before.
-    for (; currentBucketIndex < styleBucketOrdering.length; currentBucketIndex++) {
-      const nextBucket = renderer.styleElements[styleBucketOrdering[currentBucketIndex]];
-      if (nextBucket) {
-        nextBucketFromCache = nextBucket;
-        break;
-      }
+  if (bucketName === 'm' && metadata) {
+    stylesheetKey = (bucketName + metadata['m']) as string;
+  }
+
+  if (!renderer.stylesheets[stylesheetKey]) {
+    const tag: HTMLStyleElement | undefined = target && target.createElement('style');
+
+    if (bucketName === 'm' && metadata) {
+      elementAttributes['media'] = metadata['m'] as string;
     }
 
-    const tag = target.createElement('style');
+    const stylesheet = createIsomorphicStyleSheet(tag, bucketName, elementAttributes);
+    renderer.stylesheets[stylesheetKey] = stylesheet;
 
-    tag.dataset['makeStylesBucket'] = bucketName;
+    if (target && tag) {
+      let currentBucketIndex = styleBucketOrdering.indexOf(bucketName) + 1;
+      let nextBucketFromCache = null;
 
-    for (const attribute in elementAttributes) {
-      tag.setAttribute(attribute, elementAttributes[attribute]);
-    }
+      // Find the next bucket which we will add our new style bucket before.
+      for (; currentBucketIndex < styleBucketOrdering.length; currentBucketIndex++) {
+        const nextBucket = renderer.stylesheets[styleBucketOrdering[currentBucketIndex]];
+        if (nextBucket) {
+          nextBucketFromCache = nextBucket;
+          break;
+        }
+      }
 
-    renderer.styleElements[bucketName] = tag;
-    target.head.insertBefore(tag, nextBucketFromCache);
+      target.head.insertBefore(tag, nextBucketFromCache?.element || null);
+    }
   }
 
-  return renderer.styleElements[bucketName]!.sheet as CSSStyleSheet;
+  return renderer.stylesheets[stylesheetKey]!;
 }
diff --git a/packages/core/src/renderer/rehydrateRendererCache.ts b/packages/core/src/renderer/rehydrateRendererCache.ts
index 5ba2986..1961913 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.ts
@@ -1,4 +1,5 @@
 import { GriffelRenderer, StyleBucketName } from '../types';
+import { createIsomorphicStyleSheetFromElement } from './createIsomorphicStyleSheet';
 
 // Regexps to extract names of classes and animations
 // https://github.com/styletron/styletron/blob/e0fcae826744eb00ce679ac613a1b10d44256660/packages/styletron-engine-atomic/src/client/client.js#L8
@@ -9,6 +10,7 @@ const STYLES_HYDRATOR = /\.([^{:]+)(:[^{]+)?{(?:[^}]*;)?([^}]*?)}/g;
 const regexps: Partial<Record<StyleBucketName, RegExp>> = {
   k: KEYFRAMES_HYDRATOR,
   t: AT_RULES_HYDRATOR,
+  m: AT_RULES_HYDRATOR,
 };
 
 /**
@@ -28,9 +30,11 @@ export function rehydrateRendererCache(
       const bucketName = styleElement.dataset['makeStylesBucket'] as StyleBucketName;
       const regex = regexps[bucketName] || STYLES_HYDRATOR;
 
+      const stylesheetKey = bucketName === 'm' ? bucketName + styleElement.media : bucketName;
+
       // 👇 If some elements are not created yet, we will register them in renderer
-      if (!renderer.styleElements[bucketName]) {
-        renderer.styleElements[bucketName] = styleElement;
+      if (!renderer.stylesheets[stylesheetKey]) {
+        renderer.stylesheets[stylesheetKey] = createIsomorphicStyleSheetFromElement(styleElement);
       }
 
       let match;
diff --git a/packages/core/src/runtime/getStyleBucketName.ts b/packages/core/src/runtime/getStyleBucketName.ts
index 1bab581..cebd6bd 100644
--- a/packages/core/src/runtime/getStyleBucketName.ts
+++ b/packages/core/src/runtime/getStyleBucketName.ts
@@ -41,8 +41,12 @@ const pseudosMap: Record<string, StyleBucketName | undefined> = {
  * ```
  */
 export function getStyleBucketName(pseudo: string, layer: string, media: string, support: string): StyleBucketName {
-  // We are grouping all the at-rules like @media, @supports etc under `t` bucket.
-  if (media || layer || support) {
+  if (media) {
+    return 'm';
+  }
+
+  // We are grouping all the at-rules like @supports etc under `t` bucket.
+  if (layer || support) {
     return 't';
   }
 
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index 6a368db..8d0e1f8 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -2,7 +2,14 @@ import hashString from '@emotion/hash';
 import { convert, convertProperty } from 'rtl-css-js/core';
 
 import { HASH_PREFIX } from '../constants';
-import { GriffelStyle, CSSClassesMap, CSSRulesByBucket, StyleBucketName, GriffelAnimation } from '../types';
+import {
+  GriffelStyle,
+  CSSClassesMap,
+  CSSRulesByBucket,
+  StyleBucketName,
+  GriffelAnimation,
+  CSSBucketEntry,
+} from '../types';
 import { compileCSS, CompileCSSOptions } from './compileCSS';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
 import { generateCombinedQuery } from './utils/generateCombinedMediaQuery';
@@ -26,17 +33,31 @@ function pushToClassesMap(
   classesMap[propertyKey] = rtlClassname ? [ltrClassname!, rtlClassname] : ltrClassname;
 }
 
+function createBucketEntry(cssRule: string, metadata: Record<string, unknown> | undefined): CSSBucketEntry {
+  if (metadata) {
+    return [cssRule, metadata];
+  }
+
+  return cssRule;
+}
+
 function pushToCSSRules(
   cssRulesByBucket: CSSRulesByBucket,
   styleBucketName: StyleBucketName,
   ltrCSS: string,
   rtlCSS: string | undefined,
+  media: string | undefined,
 ) {
-  cssRulesByBucket[styleBucketName] = cssRulesByBucket[styleBucketName] || [];
-  cssRulesByBucket[styleBucketName]!.push(ltrCSS);
+  let metadata: Record<string, unknown> | undefined;
+  if (styleBucketName === 'm' && media) {
+    metadata = { m: media };
+  }
+
+  cssRulesByBucket[styleBucketName] ??= [];
+  cssRulesByBucket[styleBucketName]!.push(createBucketEntry(ltrCSS, metadata));
 
   if (rtlCSS) {
-    cssRulesByBucket[styleBucketName]!.push(rtlCSS);
+    cssRulesByBucket[styleBucketName]!.push(createBucketEntry(rtlCSS, metadata));
   }
 }
 
@@ -128,7 +149,7 @@ export function resolveStyleRules(
       });
 
       pushToClassesMap(cssClassesMap, key, className, rtlClassName);
-      pushToCSSRules(cssRulesByBucket, styleBucketName, ltrCSS, rtlCSS);
+      pushToCSSRules(cssRulesByBucket, styleBucketName, ltrCSS, rtlCSS, media);
     } else if (property === 'animationName') {
       const animationNameValue = Array.isArray(value) ? (value as GriffelAnimation[]) : [value as GriffelAnimation];
 
@@ -160,6 +181,7 @@ export function resolveStyleRules(
             'k',
             keyframeRules[i],
             rtlKeyframeRules[i],
+            media,
           );
         }
 
@@ -245,7 +267,7 @@ export function resolveStyleRules(
       });
 
       pushToClassesMap(cssClassesMap, key, className, rtlClassName);
-      pushToCSSRules(cssRulesByBucket, styleBucketName, ltrCSS, rtlCSS);
+      pushToCSSRules(cssRulesByBucket, styleBucketName, ltrCSS, rtlCSS, media);
     } else if (isObject(value)) {
       if (isNestedSelector(property)) {
         resolveStyleRules(
diff --git a/packages/core/src/runtime/utils/normalizeCSSBucketEntry.ts b/packages/core/src/runtime/utils/normalizeCSSBucketEntry.ts
new file mode 100644
index 0000000..bd68176
--- /dev/null
+++ b/packages/core/src/runtime/utils/normalizeCSSBucketEntry.ts
@@ -0,0 +1,19 @@
+import { CSSBucketEntry } from '../../types';
+
+/**
+ * @param entry - CSS bucket entry that can be either a string or an array
+ * @returns An array where the first element is the CSS rule
+ */
+export function normalizeCSSBucketEntry(entry: CSSBucketEntry): [string] | [string, Record<string, unknown>] {
+  if (!Array.isArray(entry)) {
+    return [entry];
+  }
+
+  if (process.env.NODE_ENV !== 'production' && entry.length > 2) {
+    throw new Error(
+      'CSS Bucket contains an entry with greater than 2 items, please report this to https://github.com/microsoft/griffel/issues',
+    );
+  }
+
+  return entry;
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ec71957..e207fbd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -85,6 +85,23 @@ export interface MakeStaticStylesOptions {
   renderer: GriffelRenderer;
 }
 
+export interface IsomorphicStyleSheet {
+  /**
+   * Attributes applied to the underlying HTMLStyleElement
+   */
+  elementAttributes: Record<string, string>;
+  /**
+   * Underlying HTMLStyleElement
+   */
+  element: HTMLStyleElement | undefined;
+  bucketName: StyleBucketName;
+  /**
+   * Returns all CSS rules on the stylesheet
+   */
+  cssRules(): string[];
+  insertRule(rule: string): number | undefined;
+}
+
 export interface GriffelRenderer {
   id: string;
 
@@ -96,7 +113,7 @@ export interface GriffelRenderer {
   /**
    * @private
    */
-  styleElements: Partial<Record<StyleBucketName, HTMLStyleElement>>;
+  stylesheets: { [key in StyleBucketName]?: IsomorphicStyleSheet } & Record<string, IsomorphicStyleSheet>;
 
   /**
    * @private
@@ -107,37 +124,41 @@ export interface GriffelRenderer {
 /**
  * Buckets under which we will group our stylesheets.
  */
-export type StyleBucketName =
+export type StyleBucketName = keyof CSSRulesByBucket;
+export type SequenceHash = string;
+export type PropertyHash = string;
+
+export type CSSClasses = /* ltrClassName */ string | [/* ltrClassName */ string, /* rtlClassName */ string];
+
+export type CSSClassesMap = Record<PropertyHash, CSSClasses>;
+export type CSSClassesMapBySlot<Slots extends string | number> = Record<Slots, CSSClassesMap>;
+
+export interface CSSRulesByBucket {
   // default
-  | 'd'
+  d?: CSSBucketEntry[];
   // link
-  | 'l'
+  l?: CSSBucketEntry[];
   // visited
-  | 'v'
+  v?: CSSBucketEntry[];
   // focus-within
-  | 'w'
+  w?: CSSBucketEntry[];
   // focus
-  | 'f'
+  f?: CSSBucketEntry[];
   // focus-visible
-  | 'i'
+  i?: CSSBucketEntry[];
   // hover
-  | 'h'
+  h?: CSSBucketEntry[];
   // active
-  | 'a'
+  a?: CSSBucketEntry[];
   // @keyframes definitions
-  | 'k'
-  // at-rules (@media, @support, @layer)
-  | 't';
-
-export type SequenceHash = string;
-export type PropertyHash = string;
-
-export type CSSClasses = /* ltrClassName */ string | [/* ltrClassName */ string, /* rtlClassName */ string];
-
-export type CSSClassesMap = Record<PropertyHash, CSSClasses>;
-export type CSSClassesMapBySlot<Slots extends string | number> = Record<Slots, CSSClassesMap>;
+  k?: CSSBucketEntry[];
+  // at-rules (@support, @layer)
+  t?: CSSBucketEntry[];
+  // @media rules
+  m?: CSSBucketEntry[];
+}
 
-export type CSSRulesByBucket = Partial<Record<StyleBucketName, string[]>>;
+export type CSSBucketEntry = string | [string, Record<string, unknown>];
 
 export type StylesBySlots<Slots extends string | number> = Record<Slots, GriffelStyle>;
 
diff --git a/packages/react/src/renderToStyleElements.ts b/packages/react/src/renderToStyleElements.ts
index 946aab4..944f5bb 100644
--- a/packages/react/src/renderToStyleElements.ts
+++ b/packages/react/src/renderToStyleElements.ts
@@ -1,8 +1,6 @@
 import { styleBucketOrdering } from '@griffel/core';
 import * as React from 'react';
-import type { GriffelRenderer, StyleBucketName } from '@griffel/core';
-
-type CSSRulesGroupedByStyleBucket = Record<StyleBucketName, string[]>;
+import type { GriffelRenderer } from '@griffel/core';
 
 /**
  * This method returns a list of <style> React elements with the rendered CSS. This is useful for Server-Side rendering.
@@ -10,37 +8,28 @@ type CSSRulesGroupedByStyleBucket = Record<StyleBucketName, string[]>;
  * @public
  */
 export function renderToStyleElements(renderer: GriffelRenderer): React.ReactElement[] {
-  const styles = styleBucketOrdering.reduce<CSSRulesGroupedByStyleBucket>((acc, bucketName) => {
-    return { ...acc, [bucketName]: [] };
-  }, {} as CSSRulesGroupedByStyleBucket);
-
-  // eslint-disable-next-line guard-for-in
-  for (const cssRule in renderer.insertionCache) {
-    const bucketName: StyleBucketName = renderer.insertionCache[cssRule];
-
-    styles[bucketName].push(cssRule);
-  }
-
-  return (Object.keys(styles) as StyleBucketName[])
-    .map(bucketName => {
-      const cssRules = styles[bucketName].join('');
-
-      // We don't want to create empty style elements
-      if (cssRules.length === 0) {
+  const stylesheets = Object.values(renderer.stylesheets).sort((a, b) => {
+    return styleBucketOrdering.indexOf(a.bucketName) - styleBucketOrdering.indexOf(b.bucketName);
+  });
+
+  return stylesheets
+    .map(stylesheet => {
+      const cssRules = stylesheet.cssRules();
+      // don't want to create any empty style elements
+      if (!cssRules.length) {
         return null;
       }
 
       return React.createElement('style', {
-        key: bucketName,
+        key: stylesheet.bucketName,
 
         // TODO: support "nonce"
         // ...renderer.styleNodeAttributes,
-
-        'data-make-styles-bucket': bucketName || 'default',
-        'data-make-styles-rehydration': true,
+        ...stylesheet.elementAttributes,
+        'data-make-styles-rehydration': 'true',
 
         dangerouslySetInnerHTML: {
-          __html: cssRules,
+          __html: cssRules.join(''),
         },
       });
     })
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/code.ts
new file mode 100644
index 0000000..3c20101
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/code.ts
@@ -0,0 +1,24 @@
+import { __styles } from '@griffel/react';
+
+const styles = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+      uwmqm3: ['fycuoez', 'f8wuabp'],
+    },
+    icon: {
+      De3pzq: 'fcnqdeg',
+      Bi91k9c: 'faf35ka',
+    },
+  },
+  {
+    d: [
+      '.fe3e8s9{color:red;}',
+      '.fycuoez{padding-left:4px;}',
+      '.f8wuabp{padding-right:4px;}',
+      '.fcnqdeg{background-color:green;}',
+    ],
+  },
+);
+
+console.log(styles);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
new file mode 100644
index 0000000..5c87bf6
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
@@ -0,0 +1,16 @@
+import { __styles, __css } from '@griffel/react';
+
+const styles = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+    uwmqm3: ['fycuoez', 'f8wuabp'],
+  },
+  icon: {
+    De3pzq: 'fcnqdeg',
+    Bi91k9c: 'faf35ka',
+  },
+});
+
+console.log(styles);
+
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fycuoez%7Bpadding-left%3A4px%3B%7D%0A.f8wuabp%7Bpadding-right%3A4px%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/code.ts
new file mode 100644
index 0000000..0b4f067
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/code.ts
@@ -0,0 +1,23 @@
+import { __styles } from '@griffel/react';
+
+export const stylesA = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+    },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
+
+export const stylesB = __styles(
+  {
+    root: {
+      De3pzq: 'fcnqdeg',
+    },
+  },
+  {
+    d: ['.fcnqdeg{background-color:green;}'],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
new file mode 100644
index 0000000..42aeb0d
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
@@ -0,0 +1,13 @@
+import { __styles, __css } from '@griffel/react';
+export const stylesA = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+  },
+});
+export const stylesB = __css({
+  root: {
+    De3pzq: 'fcnqdeg',
+  },
+});
+
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
diff --git a/packages/webpack-extraction-plugin/package.json b/packages/webpack-extraction-plugin/package.json
index 918e103..6844523 100644
--- a/packages/webpack-extraction-plugin/package.json
+++ b/packages/webpack-extraction-plugin/package.json
@@ -11,9 +11,12 @@
   "dependencies": {
     "@babel/core": "^7.12.13",
     "@babel/helper-plugin-utils": "^7.12.13",
+    "loader-utils": "^2.0.0",
+    "schema-utils": "^3.1.1",
     "tslib": "^2.1.0"
   },
   "peerDependencies": {
-    "@griffel/core": "^1.3.1"
+    "@griffel/core": "^1.3.1",
+    "webpack": "^5"
   }
 }
diff --git a/packages/webpack-extraction-plugin/project.json b/packages/webpack-extraction-plugin/project.json
index 6d65664..13cd794 100644
--- a/packages/webpack-extraction-plugin/project.json
+++ b/packages/webpack-extraction-plugin/project.json
@@ -32,6 +32,11 @@
             "glob": "LICENSE.md",
             "input": ".",
             "output": "."
+          },
+          {
+            "glob": "*.{js,css}",
+            "input": "packages/webpack-extraction-plugin/virtual-loader",
+            "output": "./virtual-loader"
           }
         ]
       }
diff --git a/packages/webpack-extraction-plugin/src/schema.ts b/packages/webpack-extraction-plugin/src/schema.ts
new file mode 100644
index 0000000..3b8433c
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/schema.ts
@@ -0,0 +1,9 @@
+import type { JSONSchema7 } from 'json-schema';
+
+export const configSchema: JSONSchema7 = {
+  $schema: 'http://json-schema.org/schema',
+  $id: 'webpackLoader-options',
+
+  properties: {},
+  additionalProperties: false,
+};
diff --git a/packages/webpack-extraction-plugin/src/transformSync.ts b/packages/webpack-extraction-plugin/src/transformSync.ts
new file mode 100644
index 0000000..ed142d1
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/transformSync.ts
@@ -0,0 +1,57 @@
+import * as Babel from '@babel/core';
+import { babelPluginStripGriffelRuntime, StripRuntimeBabelPluginMetadata } from './babelPluginStripGriffelRuntime';
+
+export type TransformOptions = {
+  filename: string;
+
+  inputSourceMap: Babel.TransformOptions['inputSourceMap'];
+  enableSourceMaps: boolean;
+};
+
+export type TransformResult = {
+  code: string;
+  cssRules: string[] | undefined;
+  sourceMap: NonNullable<Babel.BabelFileResult['map']> | undefined;
+};
+
+/**
+ * Transforms passed source code with Babel, uses user's config for parsing, but ignores it for transforms.
+ */
+export function transformSync(sourceCode: string, options: TransformOptions): TransformResult {
+  // Parse the code first so Babel will use user's babel config for parsing
+  // During transforms we don't want to use user's config
+  const babelAST = Babel.parseSync(sourceCode, {
+    caller: { name: 'griffel' },
+
+    filename: options.filename,
+    inputSourceMap: options.inputSourceMap,
+    sourceMaps: options.enableSourceMaps,
+  });
+
+  if (babelAST === null) {
+    throw new Error(`Failed to create AST for "${options.filename}" due unknown Babel error...`);
+  }
+
+  const babelFileResult = Babel.transformFromAstSync(babelAST, sourceCode, {
+    // Ignore all user's configs and apply only our plugin
+    babelrc: false,
+    configFile: false,
+    plugins: [babelPluginStripGriffelRuntime],
+
+    filename: options.filename,
+
+    sourceMaps: options.enableSourceMaps,
+    sourceFileName: options.filename,
+    inputSourceMap: options.inputSourceMap,
+  });
+
+  if (babelFileResult === null) {
+    throw new Error(`Failed to transform "${options.filename}" due unknown Babel error...`);
+  }
+
+  return {
+    code: babelFileResult.code as string,
+    cssRules: (babelFileResult.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules,
+    sourceMap: babelFileResult.map === null ? undefined : babelFileResult.map,
+  };
+}
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.ts b/packages/webpack-extraction-plugin/src/webpackLoader.ts
new file mode 100644
index 0000000..7159712
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.ts
@@ -0,0 +1,97 @@
+import { getOptions } from 'loader-utils';
+import * as path from 'path';
+import { validate } from 'schema-utils';
+import * as webpack from 'webpack';
+
+import { transformSync, TransformResult, TransformOptions } from './transformSync';
+import { configSchema } from './schema';
+
+type WebpackLoaderOptions = never;
+
+type WebpackLoaderParams = Parameters<webpack.LoaderDefinitionFunction<WebpackLoaderOptions>>;
+
+const virtualLoaderPath = path.resolve(__dirname, '..', 'virtual-loader', 'index.js');
+const resourcePath = path.resolve(__dirname, '..', 'virtual-loader', 'griffel.css');
+
+function toURIComponent(rule: string): string {
+  return encodeURIComponent(rule).replace(/!/g, '%21');
+}
+
+function shouldTransformSourceCode(sourceCode: string): boolean {
+  return sourceCode.indexOf('__styles') !== -1;
+}
+
+/**
+ * Webpack can also pass sourcemaps as a string, Babel accepts only objects.
+ * See https://github.com/babel/babel-loader/pull/889.
+ */
+function parseSourceMap(inputSourceMap: WebpackLoaderParams[1]): TransformOptions['inputSourceMap'] {
+  try {
+    if (typeof inputSourceMap === 'string') {
+      return JSON.parse(inputSourceMap) as TransformOptions['inputSourceMap'];
+    }
+
+    return inputSourceMap as TransformOptions['inputSourceMap'];
+  } catch (err) {
+    return undefined;
+  }
+}
+
+function webpackLoader(
+  this: webpack.LoaderContext<never>,
+  sourceCode: WebpackLoaderParams[0],
+  inputSourceMap: WebpackLoaderParams[1],
+) {
+  this.async();
+  // Loaders are cacheable by default, but in edge cases/bugs when caching does not work until it's specified:
+  // https://github.com/webpack/webpack/issues/14946
+  this.cacheable();
+
+  const options = getOptions(this) as WebpackLoaderOptions;
+
+  validate(configSchema, options, {
+    name: '@griffel/webpack-extraction-plugin/loader',
+    baseDataPath: 'options',
+  });
+
+  // Early return to handle cases when __styles() calls are not present, allows skipping expensive invocation of Babel
+  if (!shouldTransformSourceCode(sourceCode)) {
+    this.callback(null, sourceCode, inputSourceMap);
+    return;
+  }
+
+  let result: TransformResult | null = null;
+  let error: Error | null = null;
+
+  try {
+    result = transformSync(sourceCode, {
+      filename: path.relative(process.cwd(), this.resourcePath),
+
+      enableSourceMaps: this.sourceMap || false,
+      inputSourceMap: parseSourceMap(inputSourceMap),
+    });
+  } catch (err) {
+    error = err as Error;
+  }
+
+  if (result) {
+    if (result.cssRules) {
+      const request = `import ${JSON.stringify(
+        this.utils.contextify(
+          this.context || this.rootContext,
+          `griffel.css!=!${virtualLoaderPath}!${resourcePath}?style=${toURIComponent(result.cssRules.join('\n'))}`,
+        ),
+      )};`;
+
+      this.callback(null, `${result.code}\n\n${request};`, result.sourceMap);
+      return;
+    }
+
+    this.callback(null, result.code, result.sourceMap);
+    return;
+  }
+
+  this.callback(error);
+}
+
+export default webpackLoader;
diff --git a/packages/webpack-extraction-plugin/tsconfig.json b/packages/webpack-extraction-plugin/tsconfig.json
index e464c9e..25cc731 100644
--- a/packages/webpack-extraction-plugin/tsconfig.json
+++ b/packages/webpack-extraction-plugin/tsconfig.json
@@ -2,6 +2,8 @@
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
     "allowJs": true,
+    "checkJs": true,
+    "lib": ["es2019", "dom"],
     "forceConsistentCasingInFileNames": true,
     "strict": true,
     "noImplicitOverride": true,
diff --git a/packages/webpack-extraction-plugin/tsconfig.lib.json b/packages/webpack-extraction-plugin/tsconfig.lib.json
index 278cb18..fa6f95d 100644
--- a/packages/webpack-extraction-plugin/tsconfig.lib.json
+++ b/packages/webpack-extraction-plugin/tsconfig.lib.json
@@ -7,5 +7,5 @@
     "types": ["node", "environment"]
   },
   "exclude": ["__fixtures__/**/*/*.ts", "**/*.spec.ts", "**/*.test.ts"],
-  "include": ["**/*.ts"]
+  "include": ["**/*.ts", "virtual-loader/index.js"]
 }
diff --git a/packages/webpack-extraction-plugin/virtual-loader/griffel.css b/packages/webpack-extraction-plugin/virtual-loader/griffel.css
new file mode 100644
index 0000000..24590f7
--- /dev/null
+++ b/packages/webpack-extraction-plugin/virtual-loader/griffel.css
@@ -0,0 +1,2 @@
+/* This is a noop file for extracted CSS source to point to */
+/* Webpack requires a file to exist on disk for virtual source files */
diff --git a/packages/webpack-extraction-plugin/virtual-loader/index.js b/packages/webpack-extraction-plugin/virtual-loader/index.js
new file mode 100644
index 0000000..48d51b1
--- /dev/null
+++ b/packages/webpack-extraction-plugin/virtual-loader/index.js
@@ -0,0 +1,14 @@
+const { URLSearchParams } = require('url');
+
+/**
+ * @this {import('webpack').LoaderContext<unknown>}
+ * @return {String}
+ */
+function virtualLoader() {
+  const query = new URLSearchParams(this.resourceQuery);
+  const styleRule = query.get('style');
+
+  return styleRule ?? '';
+}
+
+module.exports = virtualLoader;


Test patch:
diff --git a/packages/core/src/makeStyles.test.ts b/packages/core/src/makeStyles.test.ts
index 71eed6a..4064348 100644
--- a/packages/core/src/makeStyles.test.ts
+++ b/packages/core/src/makeStyles.test.ts
@@ -188,7 +188,7 @@ describe('makeStyles', () => {
     // Classes emitted by different renderers can be the same
     expect(classesA).toBe(classesB);
     // Style elements should be different for different renderers
-    expect(rendererA.styleElements.d).not.toBe(rendererB.styleElements.d);
+    expect(rendererA.stylesheets.d).not.toBe(rendererB.stylesheets.d);
 
     expect(rendererA).toMatchInlineSnapshot(`
       .f22iagw {
diff --git a/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts b/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts
new file mode 100644
index 0000000..d491e5f
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts
@@ -0,0 +1,29 @@
+/*
+ * @jest-environment node
+ */
+
+// 👆 this is intentionally to test in SSR like environment
+
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
+
+describe('createIsomorphicStyleElement - node', () => {
+  it('should insert css rule', () => {
+    const stylesheet = createIsomorphicStyleSheet(undefined, 'd', {});
+    stylesheet.insertRule(".foo { color: 'red' }");
+    expect(stylesheet.cssRules()).toMatchInlineSnapshot(`
+      Array [
+        ".foo { color: 'red' }",
+      ]
+    `);
+  });
+
+  it('should set element attributes', () => {
+    const stylesheet = createIsomorphicStyleSheet(undefined, 'd', { 'data-foo': 'foo' });
+    expect(stylesheet.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-foo": "foo",
+        "data-make-styles-bucket": "d",
+      }
+    `);
+  });
+});
diff --git a/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts b/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts
new file mode 100644
index 0000000..818764c
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts
@@ -0,0 +1,47 @@
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
+
+describe('createIsomorphicStyleElement', () => {
+  it('should insert css rule', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', {});
+    stylesheet.insertRule(".foo { color: 'red' }");
+    expect(stylesheet.cssRules()).toMatchInlineSnapshot(`
+      Array [
+        ".foo { color: 'red' }",
+      ]
+    `);
+  });
+
+  it('should set element attributes', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', { 'data-foo': 'foo' });
+    expect(stylesheet.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-foo": "foo",
+        "data-make-styles-bucket": "d",
+      }
+    `);
+
+    expect(stylesheet.element).toMatchInlineSnapshot(`
+      <style
+        data-foo="foo"
+        data-make-styles-bucket="d"
+      />
+    `);
+
+    const actualElementAttributes = Array.from(stylesheet.element!.attributes).reduce((acc, attr) => {
+      acc[attr.name] = attr.value;
+      return acc;
+    }, {} as Record<string, string>);
+
+    expect(actualElementAttributes).toEqual(stylesheet.elementAttributes);
+  });
+
+  it('should create HTML style element', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', {});
+    expect(stylesheet.element).not.toBeUndefined();
+    expect(stylesheet.element).toMatchInlineSnapshot(`
+      <style
+        data-make-styles-bucket="d"
+      />
+    `);
+  });
+});
diff --git a/packages/core/src/renderer/rehydrateRendererCache.test.ts b/packages/core/src/renderer/rehydrateRendererCache.test.ts
new file mode 100644
index 0000000..faf2c68
--- /dev/null
+++ b/packages/core/src/renderer/rehydrateRendererCache.test.ts
@@ -0,0 +1,38 @@
+import { DATA_BUCKET_ATTR } from '../constants';
+import { createDOMRenderer } from './createDOMRenderer';
+import { rehydrateRendererCache } from './rehydrateRendererCache';
+
+describe('rehydrateRendererCache', () => {
+  it('should rehydrate renderer cache', () => {
+    const renderer = createDOMRenderer(document);
+    const styleElement = document.createElement('style');
+    styleElement.setAttribute(DATA_BUCKET_ATTR, 'd');
+    document.head.appendChild(styleElement);
+    styleElement.textContent = '.foo { color: red; }';
+
+    rehydrateRendererCache(renderer, document);
+
+    expect(renderer.insertionCache).toMatchInlineSnapshot(`
+      Object {
+        ".foo { color: red; }": "d",
+      }
+    `);
+  });
+
+  it('should create isomorphic stylesheet', () => {
+    const renderer = createDOMRenderer(document);
+    const styleElement = document.createElement('style');
+    document.head.appendChild(styleElement);
+    styleElement.setAttribute(DATA_BUCKET_ATTR, 'd');
+
+    rehydrateRendererCache(renderer, document);
+
+    expect(renderer.stylesheets.d).not.toBeUndefined();
+    expect(renderer.stylesheets.d?.bucketName).toMatchInlineSnapshot(`"d"`);
+    expect(renderer.stylesheets.d?.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-make-styles-bucket": "d",
+      }
+    `);
+  });
+});
diff --git a/packages/core/src/runtime/getStyleBucketName.test.ts b/packages/core/src/runtime/getStyleBucketName.test.ts
index 93239d6..052a02f 100644
--- a/packages/core/src/runtime/getStyleBucketName.test.ts
+++ b/packages/core/src/runtime/getStyleBucketName.test.ts
@@ -14,7 +14,7 @@ describe('getStyleBucketName', () => {
     expect(getStyleBucketName(':active', '', '', '')).toBe('a');
 
     expect(getStyleBucketName(':active', 'theme', '', '')).toBe('t');
-    expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('m');
     expect(getStyleBucketName(':active', '', '', '(display: table-cell)')).toBe('t');
   });
 });
diff --git a/packages/core/src/runtime/utils/normalizeCSSBucketEntry.test.ts b/packages/core/src/runtime/utils/normalizeCSSBucketEntry.test.ts
new file mode 100644
index 0000000..1793724
--- /dev/null
+++ b/packages/core/src/runtime/utils/normalizeCSSBucketEntry.test.ts
@@ -0,0 +1,21 @@
+import { normalizeCSSBucketEntry } from './normalizeCSSBucketEntry';
+
+describe('normalizeCSSBucketEntry', () => {
+  it('should handle string entry', () => {
+    const cssRule = '.foo { color: red; }';
+    expect(normalizeCSSBucketEntry(cssRule)).toEqual([cssRule]);
+  });
+
+  it('should handle array entry', () => {
+    const cssRule = '.foo { color: red; }';
+    expect(normalizeCSSBucketEntry([cssRule, {}])).toEqual([cssRule, {}]);
+  });
+
+  it('should throw if array entry contains more than 2 items', () => {
+    const cssRule = '.foo { color: red; }';
+    // @ts-expect-error types should disallow entries with more than 2 items
+    expect(() => normalizeCSSBucketEntry([cssRule, {}, {}])).toThrow(
+      'CSS Bucket contains an entry with greater than 2 items, please report this to https://github.com/microsoft/griffel/issues',
+    );
+  });
+});
diff --git a/packages/react/src/renderToStyleElements-node.test.tsx b/packages/react/src/renderToStyleElements-node.test.tsx
index 6f66fa1..354e002 100644
--- a/packages/react/src/renderToStyleElements-node.test.tsx
+++ b/packages/react/src/renderToStyleElements-node.test.tsx
@@ -69,11 +69,22 @@ describe('renderToStyleElements', () => {
           ':hover': { color: 'blue' },
         },
       },
+
+      support: {
+        '@supports (display: grid)': {
+          color: 'red',
+        },
+      },
     });
     const ExampleComponent: React.FC = () => {
       const classes = useExampleStyles();
 
-      return <div className={classes.media} />;
+      return (
+        <>
+          <div className={classes.media} />
+          <div className={classes.support} />
+        </>
+      );
     };
 
     const renderer = createDOMRenderer();
@@ -86,6 +97,16 @@ describe('renderToStyleElements', () => {
 
     expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
       <style data-make-styles-bucket="t" data-make-styles-rehydration="true">
+        @supports (display: grid) {
+          .f1ofq0jl {
+            color: red;
+          }
+        }</style
+      ><style
+        media="screen and (max-width: 992px)"
+        data-make-styles-bucket="m"
+        data-make-styles-rehydration="true"
+      >
         @media screen and (max-width: 992px) {
           .fnao3vb:hover {
             color: blue;
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.test.ts b/packages/webpack-extraction-plugin/src/webpackLoader.test.ts
new file mode 100644
index 0000000..e425fad
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.test.ts
@@ -0,0 +1,187 @@
+import * as fs from 'fs';
+import { createFsFromVolume, Volume } from 'memfs';
+import * as path from 'path';
+import * as prettier from 'prettier';
+import * as webpack from 'webpack';
+import { merge } from 'webpack-merge';
+
+type CompileOptions = {
+  webpackConfig?: webpack.Configuration;
+};
+
+const prettierConfig = JSON.parse(
+  fs.readFileSync(path.resolve(__dirname, '../../../.prettierrc'), { encoding: 'utf-8' }),
+);
+
+async function compileSourceWithWebpack(entryPath: string, options: CompileOptions): Promise<{ moduleSource: string }> {
+  const defaultConfig: webpack.Configuration = {
+    context: __dirname,
+    entry: entryPath,
+
+    mode: 'development',
+
+    output: {
+      path: path.resolve(__dirname),
+      filename: 'bundle.js',
+      pathinfo: false,
+    },
+
+    module: {
+      rules: [
+        {
+          test: /\.(ts|tsx|txt)$/,
+          include: path.dirname(entryPath),
+          use: {
+            loader: path.resolve(__dirname, './webpackLoader.ts'),
+          },
+        },
+        {
+          test: /\.css$/,
+          use: 'css-loader',
+        },
+      ],
+    },
+
+    resolve: {
+      alias: {
+        '@griffel/core': path.resolve(__dirname, '..', '..', '..', 'dist', 'packages', 'react', 'index.esm.js'),
+        '@griffel/react': path.resolve(__dirname, '..', '..', '..', 'dist', 'packages', 'react', 'index.esm.js'),
+      },
+      extensions: ['.js', '.ts'],
+    },
+  };
+
+  const webpackConfig = merge(defaultConfig, options.webpackConfig || {});
+  const compiler = webpack(webpackConfig);
+
+  const virtualFsVolume = createFsFromVolume(new Volume());
+
+  compiler.outputFileSystem = virtualFsVolume;
+  compiler.outputFileSystem.join = path.join.bind(path);
+
+  return new Promise((resolve, reject) => {
+    compiler.run((err, stats) => {
+      if (err) {
+        reject(err);
+        return;
+      }
+
+      if (typeof stats === 'undefined') {
+        reject(new Error('"stats" from Webpack are not available, unknown error...'));
+        return;
+      }
+
+      const jsonStats = stats.toJson({ source: true });
+
+      if (stats.hasErrors()) {
+        reject(stats.toJson().errors![0]);
+        return;
+      }
+
+      if (!Array.isArray(jsonStats.modules)) {
+        reject(new Error(`"stats.toJson().modules" should be an array, this could be a compilation error...`));
+        return;
+      }
+
+      const entryModule = jsonStats.modules.find(module => module.nameForCondition === entryPath);
+
+      if (!entryModule) {
+        reject(new Error(`Failed to find a fixture in "stats.toJson().modules", this could be a compilation error...`));
+        return;
+      }
+
+      resolve({ moduleSource: entryModule.source as string });
+    });
+  });
+}
+
+function fixLineEndings(value: string) {
+  return String(value).replace(/\r?\n/g, '\n').trim();
+}
+
+/**
+ * Test utility similar to "babel-plugin-tester".
+ *
+ * See https://webpack.js.org/contribute/writing-a-loader/#testing.
+ */
+function testFixture(fixtureName: string, options: CompileOptions = {}) {
+  it(`"${fixtureName}" fixture`, async () => {
+    const fixturePath = path.resolve(__dirname, '..', '__fixtures__', 'webpack', fixtureName);
+
+    const tsCodePath = path.resolve(fixturePath, 'code.ts');
+    const tsxCodePath = path.resolve(fixturePath, 'code.tsx');
+    // Specially for cases when "code" contains syntax errors
+    const txtCodePath = path.resolve(fixturePath, 'code.txt');
+
+    const tsOutputPath = path.resolve(fixturePath, 'output.ts');
+    const tsxOutputPath = path.resolve(fixturePath, 'output.tsx');
+
+    const inputPath = [
+      fs.existsSync(tsCodePath) && tsCodePath,
+      fs.existsSync(tsxCodePath) && tsxCodePath,
+      fs.existsSync(txtCodePath) && txtCodePath,
+    ].find(Boolean);
+    const outputPath = [
+      fs.existsSync(tsOutputPath) && tsOutputPath,
+      fs.existsSync(tsxOutputPath) && tsxOutputPath,
+    ].find(Boolean);
+
+    const errorPath = path.resolve(fixturePath, 'error.ts');
+    const expectedError = fs.existsSync(errorPath) && require(errorPath);
+
+    if (!inputPath) {
+      throw new Error(`Failed to find "code.{js,ts,tsx}" in "${fixturePath}"`);
+    }
+
+    if (!outputPath && !expectedError) {
+      throw new Error(`Failed to find "output.{js,ts,tsx}" or "error.ts" in "${fixturePath}"`);
+    }
+
+    if (expectedError) {
+      if (!expectedError.default) {
+        throw new Error(
+          `Please check that "error.ts" contains a default export with an error or regex in "${fixturePath}"`,
+        );
+      }
+    }
+
+    let resultModule = '';
+    let resultError: Error | webpack.StatsError = new Error();
+
+    try {
+      const result = await compileSourceWithWebpack(inputPath, options);
+
+      resultModule = fixLineEndings(
+        prettier.format(result.moduleSource, {
+          ...prettierConfig,
+          parser: 'typescript',
+        }),
+      );
+    } catch (err) {
+      if (expectedError) {
+        resultError = err as webpack.StatsError;
+      } else {
+        throw err;
+      }
+    }
+
+    if (outputPath) {
+      const moduleOutput = fixLineEndings(await fs.promises.readFile(outputPath, { encoding: 'utf8' }));
+
+      expect(resultModule).toBe(moduleOutput);
+      return;
+    }
+
+    if (expectedError) {
+      expect(resultError.message).toMatch(expectedError.default);
+    }
+  });
+}
+
+describe('webpackLoader', () => {
+  // Basic assertions
+  testFixture('basic-rules');
+
+  // Multiple
+  testFixture('multiple');
+});