Repository: microsoft/griffel (ts)

Issue:
core: add support for @container
`@container` is supported in the latest Chrome, Edge and Safari.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries

Pull request:
feat: Support CSS container queries
Adds support for `@container` queries. Container queries will be inserted in a bucket that is always after `@media` buckets.

This means that **styles in @container queries will always win over @media queries**.

There is no support for nested container queries or sorting the container query conditions yet.

Fixes #270

Patch:
diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json
index 9d9c0db..37562ce 100644
--- a/apps/website/.eslintrc.json
+++ b/apps/website/.eslintrc.json
@@ -4,7 +4,9 @@
   "overrides": [
     {
       "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
-      "rules": {}
+      "rules": {
+        "import/no-extraneous-dependencies": "off"
+      }
     },
     {
       "files": ["*.ts", "*.tsx"],
diff --git a/apps/website/docs/react/api/make-styles.md b/apps/website/docs/react/api/make-styles.md
index ab5773f..3c4888d 100644
--- a/apps/website/docs/react/api/make-styles.md
+++ b/apps/website/docs/react/api/make-styles.md
@@ -146,7 +146,7 @@ html[data-whatintent='mouse'] .f1xz3i88 {
 
 ### At-rules
 
-`@media`, `@supports` & `@layer` queries are also supported:
+`@media`, `@container`, `@supports` & `@layer` queries are also supported:
 
 ```js
 import { makeStyles } from '@griffel/react';
@@ -154,6 +154,8 @@ import { makeStyles } from '@griffel/react';
 const useClasses = makeStyles({
   root: {
     '@media screen and (max-width: 992px)': { color: 'orange' },
+    '@container (max-width: 992px)': { color: 'orange' },
+    '@container foo (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
     '@layer utility': { marginBottom: '1em' },
   },
@@ -168,6 +170,19 @@ const useClasses = makeStyles({
     color: orange;
   }
 }
+
+@container (max-width: 992px) {
+  .xx {
+    color: orange;
+  }
+}
+
+@container foo (max-width: 992px) {
+  .xx {
+    color: orange;
+  }
+}
+
 @supports (display: grid) {
   .f1ofq0jl {
     color: red;
diff --git a/apps/website/src/components/Playground/code/templates/container.js b/apps/website/src/components/Playground/code/templates/container.js
new file mode 100644
index 0000000..6d0a156
--- /dev/null
+++ b/apps/website/src/components/Playground/code/templates/container.js
@@ -0,0 +1,20 @@
+import { makeStyles, shorthands } from '@griffel/core';
+
+export default makeStyles({
+  root: {
+    '@container (max-width: 468px)': {
+      ...shorthands.gap('1.5rem'),
+      display: 'grid',
+    },
+
+    '@container foo (max-width: 468px)': {
+      ...shorthands.gap('1.5rem'),
+      display: 'grid',
+    },
+  },
+});
+
+export const meta = {
+  name: 'Container queries',
+  position: 11,
+};
diff --git a/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json b/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json
new file mode 100644
index 0000000..91bdf1e
--- /dev/null
+++ b/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Support CSS container queries",
+  "packageName": "@griffel/core",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/common/snapshotSerializers.ts b/packages/core/src/common/snapshotSerializers.ts
index 156e2e1..14856ee 100644
--- a/packages/core/src/common/snapshotSerializers.ts
+++ b/packages/core/src/common/snapshotSerializers.ts
@@ -19,7 +19,7 @@ export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
      */
     const _value = value as GriffelRenderer;
 
-    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof typeof _value['stylesheets'])[];
+    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof (typeof _value)['stylesheets'])[];
 
     const rules = stylesheetKeys.reduce((acc, styleEl) => {
       const stylesheet = _value.stylesheets[styleEl];
@@ -60,3 +60,17 @@ export const griffelRulesSerializer: jest.SnapshotSerializerPlugin = {
     }, '');
   },
 };
+
+export const griffelResetRulesSerializer: jest.SnapshotSerializerPlugin = {
+  test(value) {
+    return Array.isArray(value);
+  },
+  print(value) {
+    /**
+     * test function makes sure that value is the guarded type
+     */
+    const _value = value as [string, string | null, string[]];
+
+    return prettier.format(_value[2].join(''), { parser: 'css' }).trim();
+  },
+};
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 1b104eb..bfa18f4 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -32,6 +32,8 @@ export const styleBucketOrdering: StyleBucketName[] = [
   't',
   // @media rules
   'm',
+  // @container rules
+  'c',
 ];
 
 // avoid repeatedly calling `indexOf`to determine order during new insertions
diff --git a/packages/core/src/runtime/compileCSS.ts b/packages/core/src/runtime/compileCSS.ts
index 49a5e1d..ab33106 100644
--- a/packages/core/src/runtime/compileCSS.ts
+++ b/packages/core/src/runtime/compileCSS.ts
@@ -11,6 +11,7 @@ export interface CompileCSSOptions {
   media: string;
   layer: string;
   support: string;
+  container: string;
 
   property: string;
   value: number | string | Array<number | string>;
@@ -75,7 +76,19 @@ function createCSSRule(classNameSelector: string, cssDeclaration: string, pseudo
 }
 
 export function compileCSS(options: CompileCSSOptions): [string? /* ltr definition */, string? /* rtl definition */] {
-  const { className, media, layer, selectors, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
+  const {
+    className,
+    media,
+    layer,
+    selectors,
+    support,
+    property,
+    rtlClassName,
+    rtlProperty,
+    rtlValue,
+    value,
+    container,
+  } = options;
 
   const classNameSelector = `.${className}`;
   const cssDeclaration = Array.isArray(value)
@@ -105,5 +118,9 @@ export function compileCSS(options: CompileCSSOptions): [string? /* ltr definiti
     cssRule = `@supports ${support} { ${cssRule} }`;
   }
 
+  if (container) {
+    cssRule = `@container ${container} { ${cssRule} }`;
+  }
+
   return compileCSSRules(cssRule) as [string?, string?];
 }
diff --git a/packages/core/src/runtime/getStyleBucketName.ts b/packages/core/src/runtime/getStyleBucketName.ts
index 333de85..3f3eb51 100644
--- a/packages/core/src/runtime/getStyleBucketName.ts
+++ b/packages/core/src/runtime/getStyleBucketName.ts
@@ -47,6 +47,7 @@ export function getStyleBucketName(
   layer: string,
   media: string,
   support: string,
+  container: string,
 ): StyleBucketName {
   if (media) {
     return 'm';
@@ -57,6 +58,10 @@ export function getStyleBucketName(
     return 't';
   }
 
+  if (container) {
+    return 'c';
+  }
+
   if (selectors.length > 0) {
     const normalizedPseudo = selectors[0].trim();
 
diff --git a/packages/core/src/runtime/resolveResetStyleRules.ts b/packages/core/src/runtime/resolveResetStyleRules.ts
index 98dcb98..aef486e 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.ts
@@ -11,6 +11,7 @@ import { isObject } from './utils/isObject';
 import { hyphenateProperty } from './utils/hyphenateProperty';
 import { compileCSSRules, normalizePseudoSelector } from './compileCSS';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
+import { isContainerQuerySelector } from './utils/isContainerQuerySelector';
 import { warnAboutUnresolvedRule } from './warnings/warnAboutUnresolvedRule';
 
 /**
@@ -109,7 +110,12 @@ function createStringFromStyles(styles: GriffelResetStyle) {
         continue;
       }
 
-      if (isMediaQuerySelector(property) || isLayerSelector(property) || isSupportQuerySelector(property)) {
+      if (
+        isMediaQuerySelector(property) ||
+        isLayerSelector(property) ||
+        isSupportQuerySelector(property) ||
+        isContainerQuerySelector(property)
+      ) {
         const [ltrNested, rtlNested] = createStringFromStyles(value);
 
         ltrCSS += `${property}{${ltrNested}}`;
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index c3b6e4d..6d191f8 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -17,6 +17,7 @@ import { isMediaQuerySelector } from './utils/isMediaQuerySelector';
 import { isLayerSelector } from './utils/isLayerSelector';
 import { isNestedSelector } from './utils/isNestedSelector';
 import { isSupportQuerySelector } from './utils/isSupportQuerySelector';
+import { isContainerQuerySelector } from './utils/isContainerQuerySelector';
 import { normalizeNestedProperty } from './utils/normalizeNestedProperty';
 import { isObject } from './utils/isObject';
 import { getStyleBucketName } from './getStyleBucketName';
@@ -76,6 +77,7 @@ export function resolveStyleRules(
   media = '',
   layer = '',
   support = '',
+  container = '',
   cssClassesMap: CSSClassesMap = {},
   cssRulesByBucket: CSSRulesByBucket = {},
   rtlValue?: string,
@@ -128,7 +130,7 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(selectors, layer, media, support);
+      const styleBucketName = getStyleBucketName(selectors, layer, media, support, container);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
@@ -136,6 +138,7 @@ export function resolveStyleRules(
         selectors,
         property,
         support,
+        container,
         value,
         ...rtlCompileOptions,
       });
@@ -187,6 +190,7 @@ export function resolveStyleRules(
         media,
         layer,
         support,
+        container,
         cssClassesMap,
         cssRulesByBucket,
         rtlAnimationNames.join(', '),
@@ -246,7 +250,7 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(selectors, layer, media, support);
+      const styleBucketName = getStyleBucketName(selectors, layer, media, support, container);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
@@ -254,6 +258,7 @@ export function resolveStyleRules(
         selectors,
         property,
         support,
+        container,
         value: value as Array<string | number>,
         ...rtlCompileOptions,
       });
@@ -268,6 +273,7 @@ export function resolveStyleRules(
           media,
           layer,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -280,6 +286,7 @@ export function resolveStyleRules(
           combinedMediaQuery,
           layer,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -292,6 +299,7 @@ export function resolveStyleRules(
           media,
           combinedLayerQuery,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -304,6 +312,22 @@ export function resolveStyleRules(
           media,
           layer,
           combinedSupportQuery,
+          container,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
+      } else if (isContainerQuerySelector(property)) {
+        // TODO implement nested container queries if needed
+        // The only way to target multiple containers is to nest container queries
+        // https://developer.mozilla.org/en-US/docs/Web/CSS/@container#nested_container_queries
+        const containerQuery = property.slice(10).trim();
+        resolveStyleRules(
+          value as GriffelStyle,
+          selectors,
+          media,
+          layer,
+          support,
+          containerQuery,
           cssClassesMap,
           cssRulesByBucket,
         );
diff --git a/packages/core/src/runtime/utils/isContainerQuerySelector.ts b/packages/core/src/runtime/utils/isContainerQuerySelector.ts
new file mode 100644
index 0000000..26ceecc
--- /dev/null
+++ b/packages/core/src/runtime/utils/isContainerQuerySelector.ts
@@ -0,0 +1,3 @@
+export function isContainerQuerySelector(property: string): boolean {
+  return property.substring(0, 10) === '@container';
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index a4513c7..39fa928 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -319,6 +319,8 @@ export type CSSRulesByBucket = {
   t?: CSSBucketEntry[];
   // @media rules
   m?: CSSBucketEntry[];
+  // @container rules
+  c?: CSSBucketEntry[];
 };
 
 /** @internal */
diff --git a/packages/react/README.md b/packages/react/README.md
index a6772da..9ca0b0f 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -85,6 +85,8 @@ const useClasses = makeStyles({
     ':nth-child(2n)': { backgroundColor: '#fafafa' },
 
     '@media screen and (max-width: 992px)': { color: 'orange' },
+    '@container (max-width: 992px)': { color: 'orange' },
+    '@container foo (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
     '@layer utility': { marginBottom: '1em' },
   },


Test patch:
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.test.ts b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
index abf4186..9afcc26 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.test.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
@@ -46,6 +46,7 @@ describe('getStyleSheetForBucket', () => {
     getStyleSheetForBucket('d', target, null, renderer);
     getStyleSheetForBucket('v', target, null, renderer);
     getStyleSheetForBucket('a', target, null, renderer);
+    getStyleSheetForBucket('c', target, null, renderer);
     getStyleSheetForBucket('i', target, null, renderer);
     getStyleSheetForBucket('h', target, null, renderer);
     getStyleSheetForBucket('m', target, null, renderer);
diff --git a/packages/core/src/runtime/compileCSS.test.ts b/packages/core/src/runtime/compileCSS.test.ts
index dffa52a..50b6011 100644
--- a/packages/core/src/runtime/compileCSS.test.ts
+++ b/packages/core/src/runtime/compileCSS.test.ts
@@ -2,7 +2,7 @@ import { compileCSS, CompileCSSOptions, normalizePseudoSelector } from './compil
 
 const defaultOptions: Pick<
   CompileCSSOptions,
-  'rtlClassName' | 'className' | 'media' | 'selectors' | 'support' | 'layer'
+  'rtlClassName' | 'className' | 'media' | 'selectors' | 'support' | 'layer' | 'container'
 > = {
   className: 'foo',
   rtlClassName: 'rtl-foo',
@@ -10,6 +10,7 @@ const defaultOptions: Pick<
   selectors: [],
   support: '',
   layer: '',
+  container: '',
 };
 
 describe('compileCSS', () => {
diff --git a/packages/core/src/runtime/getStyleBucketName.test.ts b/packages/core/src/runtime/getStyleBucketName.test.ts
index becade1..f9bd051 100644
--- a/packages/core/src/runtime/getStyleBucketName.test.ts
+++ b/packages/core/src/runtime/getStyleBucketName.test.ts
@@ -2,22 +2,23 @@ import { getStyleBucketName } from './getStyleBucketName';
 
 describe('getStyleBucketName', () => {
   it('returns bucketName based on mapping', () => {
-    expect(getStyleBucketName([], '', '', '')).toBe('d');
-    expect(getStyleBucketName(['.foo'], '', '', '')).toBe('d');
+    expect(getStyleBucketName([], '', '', '', '')).toBe('d');
+    expect(getStyleBucketName(['.foo'], '', '', '', '')).toBe('d');
 
-    expect(getStyleBucketName([' :link'], '', '', '')).toBe('l');
-    expect(getStyleBucketName([' :hover '], '', '', '')).toBe('h');
+    expect(getStyleBucketName([' :link'], '', '', '', '')).toBe('l');
+    expect(getStyleBucketName([' :hover '], '', '', '', '')).toBe('h');
 
-    expect(getStyleBucketName([':link'], '', '', '')).toBe('l');
-    expect(getStyleBucketName([':visited'], '', '', '')).toBe('v');
-    expect(getStyleBucketName([':focus-within'], '', '', '')).toBe('w');
-    expect(getStyleBucketName([':focus'], '', '', '')).toBe('f');
-    expect(getStyleBucketName([':focus-visible'], '', '', '')).toBe('i');
-    expect(getStyleBucketName([':hover'], '', '', '')).toBe('h');
-    expect(getStyleBucketName([':active'], '', '', '')).toBe('a');
+    expect(getStyleBucketName([':link'], '', '', '', '')).toBe('l');
+    expect(getStyleBucketName([':visited'], '', '', '', '')).toBe('v');
+    expect(getStyleBucketName([':focus-within'], '', '', '', '')).toBe('w');
+    expect(getStyleBucketName([':focus'], '', '', '', '')).toBe('f');
+    expect(getStyleBucketName([':focus-visible'], '', '', '', '')).toBe('i');
+    expect(getStyleBucketName([':hover'], '', '', '', '')).toBe('h');
+    expect(getStyleBucketName([':active'], '', '', '', '')).toBe('a');
 
-    expect(getStyleBucketName([':active'], 'theme', '', '')).toBe('t');
-    expect(getStyleBucketName([':active'], '', '(max-width: 100px)', '')).toBe('m');
-    expect(getStyleBucketName([':active'], '', '', '(display: table-cell)')).toBe('t');
+    expect(getStyleBucketName([':active'], 'theme', '', '', '')).toBe('t');
+    expect(getStyleBucketName([':active'], '', '(max-width: 100px)', '', '')).toBe('m');
+    expect(getStyleBucketName([':active'], '', '', '(display: table-cell)', '')).toBe('t');
+    expect(getStyleBucketName([':active'], '', '', '', 'foo (max-width: 1px)')).toBe('c');
   });
 });
diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts
index e86b2b4..b2899de 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts
@@ -1,19 +1,7 @@
-import * as prettier from 'prettier';
 import { resolveResetStyleRules } from './resolveResetStyleRules';
+import { griffelResetRulesSerializer } from '../common/snapshotSerializers';
 
-expect.addSnapshotSerializer({
-  test(value) {
-    return Array.isArray(value);
-  },
-  print(value) {
-    /**
-     * test function makes sure that value is the guarded type
-     */
-    const _value = value as ReturnType<typeof resolveResetStyleRules>;
-
-    return prettier.format(_value[2].join(''), { parser: 'css' }).trim();
-  },
-});
+expect.addSnapshotSerializer(griffelResetRulesSerializer);
 
 describe('resolveResetStyleRules', () => {
   it('handles base rules', () => {
@@ -100,6 +88,38 @@ describe('resolveResetStyleRules', () => {
     `);
   });
 
+  it('handles named container queries', () => {
+    const result = resolveResetStyleRules({
+      '@container foo (max-width: 1px)': {
+        color: 'orange',
+      },
+    });
+
+    expect(result).toMatchInlineSnapshot(`
+      @container foo (max-width: 1px) {
+        .rmph5rz {
+          color: orange;
+        }
+      }
+    `);
+  });
+
+  it('handles unnamed container queries', () => {
+    const result = resolveResetStyleRules({
+      '@container (max-width: 1px)': {
+        color: 'orange',
+      },
+    });
+
+    expect(result).toMatchInlineSnapshot(`
+      @container (max-width: 1px) {
+        .r1ph1abo {
+          color: orange;
+        }
+      }
+    `);
+  });
+
   it('handles media queries', () => {
     const result = resolveResetStyleRules({
       color: 'red',
diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts
index cb97bd0..c40bac7 100644
--- a/packages/core/src/runtime/resolveStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveStyleRules.test.ts
@@ -435,6 +435,32 @@ describe('resolveStyleRules', () => {
       `);
     });
 
+    it('handles named container queries', () => {
+      const result = resolveStyleRules({ '@container foo (max-width: 1px)': { color: 'red' } });
+      expect(result).toMatchInlineSnapshot(`
+        @container foo (max-width: 1px) {
+          .fe3e8s9 {
+            color: red;
+          }
+        }
+      `);
+
+      expect(Object.keys(result[1])).toEqual(['c']);
+    });
+
+    it('handles unnamed container queries', () => {
+      const result = resolveStyleRules({ '@container (max-width: 1px)': { color: 'red' } });
+      expect(result).toMatchInlineSnapshot(`
+        @container (max-width: 1px) {
+          .fe3e8s9 {
+            color: red;
+          }
+        }
+      `);
+
+      expect(Object.keys(result[1])).toEqual(['c']);
+    });
+
     it('handles media queries', () => {
       expect(
         resolveStyleRules({