Repository: microsoft/griffel
Pull request #140 (2022-06-17 07:43:37)

feat: Insert media queries into separate buckets
This PR introduces the concept of 'nested buckets' Media queries will be inserted into separate buckets based on the media query value. However, to keep existing categorization, media query buckets will all have a bucket value of `m`. Media buckets are always inserted after any other bucket. **Sorting the media buckets is out of scope of this PR**

```html
<! -- Before -->
<style data-make-styles-bucket="t">
  @media screen and (max-width: 1px) {
    .foo { color: red; }
  }

  @media screen and (max-width: 2px) {
    .foo { color: blue; }
  }

  @supports (display: grid) {
    div {
      display: grid;
    }
  }
</style>

<! -- After-->
<style data-make-styles-bucket="t">
  @supports (display: grid) {
    div {
      display: grid;
    }
  }
</style>

<style data-make-styles-bucket="m" media="screen and (max-width: 1px)">
  @media screen and (max-width: 1px) {
    .foo { color: red; }
  }
</style>

<style data-make-styles-bucket="m" media="screen and (max-width: 2px)">
  @media screen and (max-width: 2px) {
    .foo { color: blue; }
  }
</style>
```

Adds the concept of 'metadata' for entries in the CSS buckets. This allows the possibility of the bucket entry to be an array with an optional metadata object as the second array element. Currently only utilized for the media query buckets.

```js
// Before
const cssRulesByBucket = {
  d:[ '.foo { color: red; }' ],
  t: [ '@media screen and (max-width: 1px) { .foo { color: red; } }' ]
}

// After
const cssRulesByBucket = {
  d: [ '.foo { color: red; }' ],
  m: [ '@media screen and (max-width: 1px) { .foo { color: red; } }', metadata: { m: '@media screen and (max-width: 1px)' }]
}
```

Addresses: #129

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

| Package & Exports                                                                                                            |    Baseline (minified/GZIP) |                          PR |                                                                                                                                                                                                   Change |
| :--------------------------------------------------------------------------------------------------------------------------- | --------------------------: | --------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStylesRuntime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr>   | `20.269 kB`<br />`7.549 kB` | `20.394 kB`<br />`7.612 kB` |  `125 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`63 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStaticStyles-runtime.fixture.js'>makeStaticStyles (runtime)</abbr>    |  `8.533 kB`<br />`3.706 kB` |   `8.636 kB`<br />`3.77 kB` |  `103 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`64 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles-runtime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr> | `21.586 kB`<br />`8.082 kB` |   `21.814 kB`<br />`8.2 kB` | `228 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`118 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>      |  `3.095 kB`<br />`1.394 kB` |  `3.198 kB`<br />`1.446 kB` |  `103 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`52 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |

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

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

<!-- Sticky Pull Request Comment -->

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-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 1c9e92e..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
@@ -21,12 +22,12 @@ export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
     const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof typeof _value['stylesheets'])[];
 
     const rules = stylesheetKeys.reduce((acc, styleEl) => {
-      const stylesheet = _value.stylesheets[styleEl]?.element;
+      const stylesheet = _value.stylesheets[styleEl];
 
       if (stylesheet) {
-        const cssRules = stylesheet.sheet ? Array.from(stylesheet.sheet.cssRules) : [];
+        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/renderer/createDOMRenderer.ts b/packages/core/src/renderer/createDOMRenderer.ts
index bbf8b59..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';
 
@@ -43,16 +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 = 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;
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 7d78e98..de8134e 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -27,6 +27,8 @@ export const styleBucketOrdering: StyleBucketName[] = [
   'k',
   // at-rules
   't',
+  // @media rules
+  'm',
 ];
 
 /**
@@ -37,11 +39,23 @@ export function getStyleSheetForBucket(
   target: Document | undefined,
   renderer: GriffelRenderer,
   elementAttributes: Record<string, string> = {},
+  metadata?: Record<string, unknown>,
 ): IsomorphicStyleSheet {
-  if (!renderer.stylesheets[bucketName]) {
+  let stylesheetKey: StyleBucketName | string = bucketName;
+
+  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 stylesheet = createIsomorphicStyleSheet(tag, bucketName, elementAttributes);
-    renderer.stylesheets[bucketName] = stylesheet;
+    renderer.stylesheets[stylesheetKey] = stylesheet;
 
     if (target && tag) {
       let currentBucketIndex = styleBucketOrdering.indexOf(bucketName) + 1;
@@ -60,5 +74,5 @@ export function getStyleSheetForBucket(
     }
   }
 
-  return renderer.stylesheets[bucketName]!;
+  return renderer.stylesheets[stylesheetKey]!;
 }
diff --git a/packages/core/src/renderer/rehydrateRendererCache.ts b/packages/core/src/renderer/rehydrateRendererCache.ts
index 00a36d5..1961913 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.ts
@@ -10,6 +10,7 @@ const STYLES_HYDRATOR = /\.([^{:]+)(:[^{]+)?{(?:[^}]*;)?([^}]*?)}/g;
 const regexps: Partial<Record<StyleBucketName, RegExp>> = {
   k: KEYFRAMES_HYDRATOR,
   t: AT_RULES_HYDRATOR,
+  m: AT_RULES_HYDRATOR,
 };
 
 /**
@@ -29,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.stylesheets[bucketName]) {
-        renderer.stylesheets[bucketName] = createIsomorphicStyleSheetFromElement(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 7d370c1..e207fbd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -113,7 +113,7 @@ export interface GriffelRenderer {
   /**
    * @private
    */
-  stylesheets: Partial<Record<StyleBucketName, IsomorphicStyleSheet>>;
+  stylesheets: { [key in StyleBucketName]?: IsomorphicStyleSheet } & Record<string, IsomorphicStyleSheet>;
 
   /**
    * @private
@@ -124,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 bd82a73..944f5bb 100644
--- a/packages/react/src/renderToStyleElements.ts
+++ b/packages/react/src/renderToStyleElements.ts
@@ -13,19 +13,19 @@ export function renderToStyleElements(renderer: GriffelRenderer): React.ReactEle
   });
 
   return stylesheets
-    .map(styleElement => {
-      const cssRules = styleElement.cssRules();
+    .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: styleElement.bucketName,
+        key: stylesheet.bucketName,
 
         // TODO: support "nonce"
         // ...renderer.styleNodeAttributes,
-        ...styleElement.elementAttributes,
+        ...stylesheet.elementAttributes,
         'data-make-styles-rehydration': 'true',
 
         dangerouslySetInnerHTML: {


Test patch:
diff --git a/packages/core/src/renderer/rehydrateRendererCache.test.ts b/packages/core/src/renderer/rehydrateRendererCache.test.ts
index 810e9d3..faf2c68 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.test.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.test.ts
@@ -24,6 +24,7 @@ describe('rehydrateRendererCache', () => {
     const styleElement = document.createElement('style');
     document.head.appendChild(styleElement);
     styleElement.setAttribute(DATA_BUCKET_ATTR, 'd');
+
     rehydrateRendererCache(renderer, document);
 
     expect(renderer.stylesheets.d).not.toBeUndefined();
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;