Repository: microsoft/griffel (ts)

Issue:
feature(core): move at-rules in reset styles to separate bucket
The reset styles bucket has the least specificity compared to any other bucket of `makeStyles`. This can cause some un-intentional bugs. One one serious problem might be with media queries https://codesandbox.io/s/cranky-cdn-4tvycj?file=/src/App.js.

It could be argued that users might expect media queries to 'win'. If used within reset styles (i.e. in a library), this could be an antipattern since any other override will override those styles.

Some possible suggestions:
* Add more examples of consequences about reset styles specificity limitations
* Add lint rule to avoid at-rules in `makeResetStyles`
* Don't support At-rules in reset styles

Pull request:
fix(core): makeResetStyles emits rules into separate buckets
Fixes #403.

This PR introduces a new bucket ("s") for [at rules](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule) in `makeResetStyles`.

### Before

```html
<style data-bucket="r">
  /** rules by makeResetStyles */
  .foo { color: red }
  @media (x) { color: green }
</style>
<style data-bucket="d">
  /** rules by makeStyles */
  .atomic { color: pink }
</style>
```

❌ The problem is `atomic` will override `@media (x)` rule because of the order of appearance.

### After

```html
<style data-bucket="r">
  /** rules by makeResetStyles */
  .foo { color: red }
</style>
<style data-bucket="d">
  /** rules by makeStyles */
  .atomic { color: pink }
</style>
<style data-bucket="s">
  /** rules by makeResetStyles */
  .foo { color: red }
  @media (x) { color: green }
</style>
```

✅ `atomic` overrides `foo`, but does not override `@media (x)`.

----

### Related changes

- Changes to `makeResetStyles` & `resolveResetStyleRules` were done in backwards compat way, so styles compiled by previous versions on Babel preset can be still handled by tooling
- Babel preset & Extraction plugin were updated to handle new syntax

Patch:
diff --git a/change/@griffel-core-9d917d14-e85c-4ca7-beb7-164dcb0d6085.json b/change/@griffel-core-9d917d14-e85c-4ca7-beb7-164dcb0d6085.json
new file mode 100644
index 0000000..fa555e0
--- /dev/null
+++ b/change/@griffel-core-9d917d14-e85c-4ca7-beb7-164dcb0d6085.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: makeResetStyles emits at rules into a separate bucket",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-a850ebd1-fc07-46c7-b8c7-3c97f8cfe864.json b/change/@griffel-react-a850ebd1-fc07-46c7-b8c7-3c97f8cfe864.json
new file mode 100644
index 0000000..d807154
--- /dev/null
+++ b/change/@griffel-react-a850ebd1-fc07-46c7-b8c7-3c97f8cfe864.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: add support for different buckets in makeResetStyles",
+  "packageName": "@griffel/react",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-webpack-extraction-plugin-2982acb5-12ca-4655-8e1c-f048c923919f.json b/change/@griffel-webpack-extraction-plugin-2982acb5-12ca-4655-8e1c-f048c923919f.json
new file mode 100644
index 0000000..c1e1280
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-2982acb5-12ca-4655-8e1c-f048c923919f.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: add support for different buckets in makeResetStyles",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/babel-preset/__fixtures__/reset-styles-at-rules/code.ts b/packages/babel-preset/__fixtures__/reset-styles-at-rules/code.ts
new file mode 100644
index 0000000..eacfc76
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/reset-styles-at-rules/code.ts
@@ -0,0 +1,7 @@
+import { makeResetStyles } from '@griffel/react';
+
+export const useStyles = makeResetStyles({
+  color: 'red',
+  '@supports (display: flex)': { color: 'pink' },
+  '@media (min-width: 100px)': { color: 'blue' },
+});
diff --git a/packages/babel-preset/__fixtures__/reset-styles-at-rules/output.ts b/packages/babel-preset/__fixtures__/reset-styles-at-rules/output.ts
new file mode 100644
index 0000000..2495b0f
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/reset-styles-at-rules/output.ts
@@ -0,0 +1,5 @@
+import { __resetStyles } from '@griffel/react';
+export const useStyles = __resetStyles('rjrhw4c', null, {
+  r: ['.rjrhw4c{color:red;}'],
+  s: ['@supports (display: flex){.rjrhw4c{color:pink;}}', '@media (min-width: 100px){.rjrhw4c{color:blue;}}'],
+});
diff --git a/packages/core/src/__resetStyles.ts b/packages/core/src/__resetStyles.ts
index 2d57114..138997f 100644
--- a/packages/core/src/__resetStyles.ts
+++ b/packages/core/src/__resetStyles.ts
@@ -1,7 +1,7 @@
 import { DEBUG_RESET_CLASSES } from './constants';
 import { insertionFactory } from './insertionFactory';
 import type { MakeResetStylesOptions } from './makeResetStyles';
-import type { GriffelInsertionFactory } from './types';
+import type { CSSRulesByBucket, GriffelInsertionFactory } from './types';
 
 /**
  * @internal
@@ -9,7 +9,7 @@ import type { GriffelInsertionFactory } from './types';
 export function __resetStyles(
   ltrClassName: string,
   rtlClassName: string | null,
-  cssRules: string[],
+  cssRules: CSSRulesByBucket | string[],
   factory: GriffelInsertionFactory = insertionFactory,
 ) {
   const insertStyles = factory();
@@ -18,7 +18,7 @@ export function __resetStyles(
     const { dir, renderer } = options;
     const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;
 
-    insertStyles(renderer, { r: cssRules });
+    insertStyles(renderer, Array.isArray(cssRules) ? { r: cssRules! } : cssRules!);
 
     if (process.env.NODE_ENV !== 'production') {
       DEBUG_RESET_CLASSES[className] = 1;
diff --git a/packages/core/src/common/snapshotSerializers.ts b/packages/core/src/common/snapshotSerializers.ts
index c66fefb..c507ebc 100644
--- a/packages/core/src/common/snapshotSerializers.ts
+++ b/packages/core/src/common/snapshotSerializers.ts
@@ -77,9 +77,10 @@ export const griffelResetRulesSerializer: jest.SnapshotSerializerPlugin = {
      * test function makes sure that value is the guarded type
      */
     const _value = value as ReturnType<typeof resolveResetStyleRules>;
-    const cssRulesByBucket: CSSRulesByBucket = { r: _value[2] };
+    const cssRulesByBucket: CSSRulesByBucket = Array.isArray(_value[2]) ? { r: _value[2] } : _value[2];
 
     return Object.entries(cssRulesByBucket)
+      .filter(([key, value]) => value.length > 0)
       .flatMap(([key, value]) => [`/** bucket "${key}" */`, format(value as string[])])
       .join('\n');
   },
diff --git a/packages/core/src/makeResetStyles.ts b/packages/core/src/makeResetStyles.ts
index a9ed726..2de5158 100644
--- a/packages/core/src/makeResetStyles.ts
+++ b/packages/core/src/makeResetStyles.ts
@@ -3,8 +3,7 @@ import type { GriffelResetStyle } from '@griffel/style-types';
 import { DEBUG_RESET_CLASSES } from './constants';
 import { insertionFactory } from './insertionFactory';
 import { resolveResetStyleRules } from './runtime/resolveResetStyleRules';
-import type { GriffelRenderer } from './types';
-import type { GriffelInsertionFactory } from './types';
+import type { CSSRulesByBucket, GriffelRenderer, GriffelInsertionFactory } from './types';
 
 export interface MakeResetStylesOptions {
   dir: 'ltr' | 'rtl';
@@ -17,7 +16,7 @@ export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInser
   let ltrClassName: string | null = null;
   let rtlClassName: string | null = null;
 
-  let cssRules: string[] | null = null;
+  let cssRules: CSSRulesByBucket | string[] | null = null;
 
   function computeClassName(options: MakeResetStylesOptions): string {
     const { dir, renderer } = options;
@@ -26,7 +25,7 @@ export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInser
       [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles);
     }
 
-    insertStyles(renderer, { r: cssRules! });
+    insertStyles(renderer, Array.isArray(cssRules) ? { r: cssRules! } : cssRules!);
 
     const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;
 
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index bfa18f4..8418bac 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -26,6 +26,8 @@ export const styleBucketOrdering: StyleBucketName[] = [
   'h',
   // active
   'a',
+  // at rules for reset styles
+  's',
   // keyframes
   'k',
   // at-rules
diff --git a/packages/core/src/runtime/compileResetCSSRules.ts b/packages/core/src/runtime/compileResetCSSRules.ts
new file mode 100644
index 0000000..a5ebd27
--- /dev/null
+++ b/packages/core/src/runtime/compileResetCSSRules.ts
@@ -0,0 +1,34 @@
+import { compile, middleware, serialize, stringify } from 'stylis';
+
+import { globalPlugin } from './stylis/globalPlugin';
+import { isAtRuleElement } from './stylis/isAtRuleElement';
+import { prefixerPlugin } from './stylis/prefixerPlugin';
+import { rulesheetPlugin } from './stylis/rulesheetPlugin';
+
+export function compileResetCSSRules(cssRules: string): [string[], string[]] {
+  const rules: string[] = [];
+  const atRules: string[] = [];
+
+  serialize(
+    compile(cssRules),
+    middleware([
+      globalPlugin,
+      prefixerPlugin,
+      stringify,
+
+      // 💡 we are using `.insertRule()` API for DOM operations, which does not support
+      // insertion of multiple CSS rules in a single call. `rulesheet` plugin extracts
+      // individual rules to be used with this API
+      rulesheetPlugin((element, rule) => {
+        if (isAtRuleElement(element)) {
+          atRules.push(rule);
+          return;
+        }
+
+        rules.push(rule);
+      }),
+    ]),
+  );
+
+  return [rules, atRules];
+}
diff --git a/packages/core/src/runtime/resolveResetStyleRules.ts b/packages/core/src/runtime/resolveResetStyleRules.ts
index 318e25d..ffb5b84 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.ts
@@ -3,6 +3,7 @@ import type { GriffelResetStyle, GriffelAnimation } from '@griffel/style-types';
 import { convert, convertProperty } from 'rtl-css-js/core';
 
 import { RESET_HASH_PREFIX } from '../constants';
+import type { CSSRulesByBucket } from '../types';
 import { isMediaQuerySelector } from './utils/isMediaQuerySelector';
 import { isLayerSelector } from './utils/isLayerSelector';
 import { isNestedSelector } from './utils/isNestedSelector';
@@ -10,7 +11,7 @@ import { isSupportQuerySelector } from './utils/isSupportQuerySelector';
 import { isObject } from './utils/isObject';
 import { hyphenateProperty } from './utils/hyphenateProperty';
 import { normalizePseudoSelector } from './compileAtomicCSSRule';
-import { compileCSSRules } from './compileCSSRules';
+import { compileResetCSSRules } from './compileResetCSSRules';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
 import { isContainerQuerySelector } from './utils/isContainerQuerySelector';
 import { warnAboutUnresolvedRule } from './warnings/warnAboutUnresolvedRule';
@@ -135,18 +136,26 @@ function createStringFromStyles(styles: GriffelResetStyle) {
 /**
  * @internal
  */
-export function resolveResetStyleRules(styles: GriffelResetStyle): [string, string | null, string[]] {
+export function resolveResetStyleRules(
+  styles: GriffelResetStyle,
+): [string, string | null, CSSRulesByBucket | string[]] {
   const [ltrRule, rtlRule] = createStringFromStyles(styles);
 
   const ltrClassName = RESET_HASH_PREFIX + hashString(ltrRule);
-  const ltrCSS = compileCSSRules(`.${ltrClassName}{${ltrRule}}`, false);
+  const [ltrCSS, ltrCSSAtRules] = compileResetCSSRules(`.${ltrClassName}{${ltrRule}}`);
+
+  const hasAtRules = ltrCSSAtRules.length > 0;
 
   if (ltrRule === rtlRule) {
-    return [ltrClassName, null, ltrCSS];
+    return [ltrClassName, null, hasAtRules ? { r: ltrCSS, s: ltrCSSAtRules } : ltrCSS];
   }
 
   const rtlClassName = RESET_HASH_PREFIX + hashString(rtlRule);
-  const rtlCSS = compileCSSRules(`.${rtlClassName}{${rtlRule}}`, false);
+  const [rtlCSS, rtlCSSAtRules] = compileResetCSSRules(`.${rtlClassName}{${rtlRule}}`);
 
-  return [ltrClassName, rtlClassName, ltrCSS.concat(rtlCSS)];
+  return [
+    ltrClassName,
+    rtlClassName,
+    hasAtRules ? { r: ltrCSS.concat(rtlCSS), s: ltrCSSAtRules.concat(rtlCSSAtRules) } : ltrCSS.concat(rtlCSS),
+  ];
 }
diff --git a/packages/core/src/runtime/stylis/isAtRuleElement.ts b/packages/core/src/runtime/stylis/isAtRuleElement.ts
new file mode 100644
index 0000000..b6b32b1
--- /dev/null
+++ b/packages/core/src/runtime/stylis/isAtRuleElement.ts
@@ -0,0 +1,14 @@
+import { LAYER, MEDIA, SUPPORTS } from 'stylis';
+import type { Element } from 'stylis';
+
+export function isAtRuleElement(element: Element): boolean {
+  switch (element.type) {
+    case '@container':
+    case MEDIA:
+    case SUPPORTS:
+    case LAYER:
+      return true;
+  }
+
+  return false;
+}
diff --git a/packages/core/src/runtime/stylis/rulesheetPlugin.ts b/packages/core/src/runtime/stylis/rulesheetPlugin.ts
new file mode 100644
index 0000000..49419e6
--- /dev/null
+++ b/packages/core/src/runtime/stylis/rulesheetPlugin.ts
@@ -0,0 +1,16 @@
+import type { Element, Middleware } from 'stylis';
+
+export type RulesheetPluginCallback = (type: Element, rule: string) => void;
+
+/**
+ * The same plugin as in stylis, but this version also has "element" argument.
+ */
+export function rulesheetPlugin(callback: RulesheetPluginCallback): Middleware {
+  return function (element) {
+    if (!element.root) {
+      if (element.return) {
+        callback(element, element.return);
+      }
+    }
+  };
+}
diff --git a/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.ts b/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.ts
index 030aa35..455b2f4 100644
--- a/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.ts
+++ b/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.ts
@@ -1,13 +1,8 @@
-import { LAYER, MEDIA, SUPPORTS } from 'stylis';
 import type { Middleware } from 'stylis';
+import { isAtRuleElement } from './isAtRuleElement';
 
 export const sortClassesInAtRulesPlugin: Middleware = element => {
-  switch (element.type) {
-    case MEDIA:
-    case SUPPORTS:
-    case LAYER:
-      if (Array.isArray(element.children)) {
-        element.children.sort((a, b) => (a.props[0] > b.props[0] ? 1 : -1));
-      }
+  if (isAtRuleElement(element) && Array.isArray(element.children)) {
+    element.children.sort((a, b) => (a.props[0] > b.props[0] ? 1 : -1));
   }
 };
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 0f91f2e..0df1025 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -77,6 +77,8 @@ export type CSSRulesByBucket = {
   h?: CSSBucketEntry[];
   // active
   a?: CSSBucketEntry[];
+  // at rules for reset
+  s?: CSSBucketEntry[];
   // @keyframes definitions
   k?: CSSBucketEntry[];
   // at-rules (@support, @layer)
diff --git a/packages/react/src/__resetStyles.ts b/packages/react/src/__resetStyles.ts
index 7bad4e0..574a998 100644
--- a/packages/react/src/__resetStyles.ts
+++ b/packages/react/src/__resetStyles.ts
@@ -1,4 +1,5 @@
 import { __resetStyles as vanillaResetStyles } from '@griffel/core';
+import type { CSSRulesByBucket } from '@griffel/core';
 
 import { useRenderer } from './RendererContext';
 import { useTextDirection } from './TextDirectionContext';
@@ -9,7 +10,11 @@ import { useTextDirection } from './TextDirectionContext';
  * @internal
  */
 // eslint-disable-next-line @typescript-eslint/naming-convention
-export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) {
+export function __resetStyles(
+  ltrClassName: string,
+  rtlClassName: string | null,
+  cssRules: CSSRulesByBucket | string[],
+) {
   const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules);
 
   return function useClasses(): string {
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/code.ts
index 30545ef..161d862 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/code.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/code.ts
@@ -1,5 +1,5 @@
 import { __resetStyles } from '@griffel/react';
-export const useClassName = __resetStyles('r13wlxb8', null, [
-  '.r13wlxb8{line-height:0;}',
-  '@media screen and (prefers-reduced-motion: reduce){.r13wlxb8{transition-duration:0.01ms;}}',
-]);
+export const useClassName = __resetStyles('rjrhw4c', null, {
+  r: ['.rjrhw4c{color:red;}'],
+  s: ['@supports (display: flex){.rjrhw4c{color:pink;}}', '@media (min-width: 100px){.rjrhw4c{color:blue;}}'],
+});
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/output.css
index 20c9f1e..b8c0f89 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/output.css
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-media/output.css
@@ -1,9 +1,14 @@
 /** griffel.css **/
-.r13wlxb8 {
-  line-height: 0;
+.rjrhw4c {
+  color: red;
 }
-@media screen and (prefers-reduced-motion: reduce) {
-  .r13wlxb8 {
-    transition-duration: 0.01ms;
+@supports (display: flex) {
+  .rjrhw4c {
+    color: pink;
+  }
+}
+@media (min-width: 100px) {
+  .rjrhw4c {
+    color: blue;
   }
 }
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
index 02f0f04..ff5cc1b 100644
--- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -47,9 +47,12 @@ function evaluateAndUpdateArgument(
 
     state.cssRulesByBucket = concatCSSRulesByBucket(state.cssRulesByBucket, cssRulesByBucket);
   } else if (functionKind === '__resetStyles') {
-    const cssRules = evaluationResult.value as NonNullable<CSSRulesByBucket['r']>;
+    const cssRules = evaluationResult.value as CSSRulesByBucket | string[];
 
-    state.cssRulesByBucket = concatCSSRulesByBucket(state.cssRulesByBucket, { r: cssRules });
+    state.cssRulesByBucket = concatCSSRulesByBucket(
+      state.cssRulesByBucket,
+      Array.isArray(cssRules) ? { r: cssRules } : cssRules,
+    );
   }
 
   argumentPath.remove();
@@ -68,7 +71,7 @@ function getFunctionArgumentPath(
   }
 
   if (functionKind === '__resetStyles') {
-    if (argumentPaths.length === 3 && argumentPaths[2].isArrayExpression()) {
+    if (argumentPaths.length === 3 && (argumentPaths[2].isArrayExpression() || argumentPaths[2].isObjectExpression())) {
       return argumentPaths[2];
     }
   }


Test patch:
diff --git a/packages/babel-preset/src/transformPlugin.test.ts b/packages/babel-preset/src/transformPlugin.test.ts
index f2ed1a3..c038021 100644
--- a/packages/babel-preset/src/transformPlugin.test.ts
+++ b/packages/babel-preset/src/transformPlugin.test.ts
@@ -181,6 +181,11 @@ pluginTester({
       fixture: path.resolve(fixturesDir, 'assets-reset-styles', 'code.ts'),
       outputFixture: path.resolve(fixturesDir, 'assets-reset-styles', 'output.ts'),
     },
+    {
+      title: 'reset: at rules',
+      fixture: path.resolve(fixturesDir, 'reset-styles-at-rules', 'code.ts'),
+      outputFixture: path.resolve(fixturesDir, 'reset-styles-at-rules', 'output.ts'),
+    },
 
     // Imports
     //
diff --git a/packages/core/src/makeResetStyles.test.ts b/packages/core/src/makeResetStyles.test.ts
index c548250..2cbf842 100644
--- a/packages/core/src/makeResetStyles.test.ts
+++ b/packages/core/src/makeResetStyles.test.ts
@@ -51,4 +51,25 @@ describe('makeResetStyles', () => {
       }
     `);
   });
+
+  it('handles at rules', () => {
+    const computeClassName = makeResetStyles({
+      color: 'red',
+      '@media (min-width: 100px)': { color: 'blue' },
+    });
+
+    expect(computeClassName({ dir: 'ltr', renderer })).toEqual('rbwcbv2');
+    expect(renderer).toMatchInlineSnapshot(`
+      /** bucket "r" **/
+      .rbwcbv2 {
+        color: red;
+      }
+      /** bucket "s" **/
+      @media (min-width: 100px) {
+        .rbwcbv2 {
+          color: blue;
+        }
+      }
+    `);
+  });
 });
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.test.ts b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
index 04685b6..082b27d 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.test.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
@@ -54,6 +54,7 @@ describe('getStyleSheetForBucket', () => {
     getStyleSheetForBucket('t', target, null, renderer);
     getStyleSheetForBucket('k', target, null, renderer);
     getStyleSheetForBucket('f', target, null, renderer);
+    getStyleSheetForBucket('s', target, null, renderer);
 
     const styleElements = target.head.querySelectorAll(`[${DATA_BUCKET_ATTR}]`);
     const styleElementOrder = Array.from(styleElements).map(styleElement =>
diff --git a/packages/core/src/runtime/compileResetCSSRules.test.ts b/packages/core/src/runtime/compileResetCSSRules.test.ts
new file mode 100644
index 0000000..6e09f83
--- /dev/null
+++ b/packages/core/src/runtime/compileResetCSSRules.test.ts
@@ -0,0 +1,23 @@
+import { compileResetCSSRules } from './compileResetCSSRules';
+
+describe('compileCSSRules', () => {
+  it('compiles CSS rules', () => {
+    const cssRules = `
+      .foo {
+        color: red;
+        @media (min-width: 768px) { color: blue }
+      }
+    `;
+
+    expect(compileResetCSSRules(cssRules)).toMatchInlineSnapshot(`
+      Array [
+        Array [
+          ".foo{color:red;}",
+        ],
+        Array [
+          "@media (min-width: 768px){.foo{color:blue;}}",
+        ],
+      ]
+    `);
+  });
+});
diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts
index ce2a533..14f45fd 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts
@@ -102,7 +102,7 @@ describe('resolveResetStyleRules', () => {
     });
 
     expect(result).toMatchInlineSnapshot(`
-      /** bucket "r" */
+      /** bucket "s" */
       @container foo (max-width: 1px) {
         .rmph5rz {
           color: orange;
@@ -119,7 +119,7 @@ describe('resolveResetStyleRules', () => {
     });
 
     expect(result).toMatchInlineSnapshot(`
-      /** bucket "r" */
+      /** bucket "s" */
       @container (max-width: 1px) {
         .r1ph1abo {
           color: orange;
@@ -144,6 +144,7 @@ describe('resolveResetStyleRules', () => {
       .rpycl1b {
         color: red;
       }
+      /** bucket "s" */
       @media (forced-colors: active) {
         .rpycl1b {
           color: orange;
@@ -166,7 +167,7 @@ describe('resolveResetStyleRules', () => {
     });
 
     expect(result).toMatchInlineSnapshot(`
-      /** bucket "r" */
+      /** bucket "s" */
       @layer utilities {
         .rvhnavh {
           color: orange;
@@ -189,7 +190,7 @@ describe('resolveResetStyleRules', () => {
     });
 
     expect(result).toMatchInlineSnapshot(`
-      /** bucket "r" */
+      /** bucket "s" */
       @supports (display: flex) {
         .rxf8lon {
           color: orange;
@@ -213,7 +214,7 @@ describe('resolveResetStyleRules', () => {
     });
 
     expect(result).toMatchInlineSnapshot(`
-      /** bucket "r" */
+      /** bucket "s" */
       @supports (display: flex) {
         .rhd25ja {
           color: pink;
diff --git a/packages/core/src/runtime/stylis/isAtRuleElement.test.ts b/packages/core/src/runtime/stylis/isAtRuleElement.test.ts
new file mode 100644
index 0000000..5639f1a
--- /dev/null
+++ b/packages/core/src/runtime/stylis/isAtRuleElement.test.ts
@@ -0,0 +1,15 @@
+import { compile } from 'stylis';
+import { isAtRuleElement } from './isAtRuleElement';
+
+describe('isAtRuleElement', () => {
+  it.each([
+    ['@container { div { color: red } }', true],
+    ['@layer foo { div { color: red } }', true],
+    ['@media (min-width: 100px) { div { color: red } }', true],
+    ['@supports (display: flex) { div { color: red } }', true],
+    ['div { color: red }', false],
+    ['div:hover { color: red }', false],
+  ])('handles "%s"', (css, expected) => {
+    expect(isAtRuleElement(compile(css)[0])).toBe(expected);
+  });
+});
diff --git a/packages/core/src/runtime/stylis/rulesheetPlugin.test.ts b/packages/core/src/runtime/stylis/rulesheetPlugin.test.ts
new file mode 100644
index 0000000..84867e8
--- /dev/null
+++ b/packages/core/src/runtime/stylis/rulesheetPlugin.test.ts
@@ -0,0 +1,30 @@
+import { compile, middleware, serialize, stringify, RULESET } from 'stylis';
+
+import { rulesheetPlugin } from './rulesheetPlugin';
+import type { RulesheetPluginCallback } from './rulesheetPlugin';
+
+function compileRule(rule: string, callback: RulesheetPluginCallback) {
+  return serialize(compile(rule), middleware([stringify, rulesheetPlugin(callback)]));
+}
+
+describe('rulesheetPlugin', () => {
+  it('handles basic selectors', () => {
+    const callback = jest.fn();
+    const css = `
+      .foo { color: red }
+      .bar { color: blue }
+    `;
+
+    expect(compileRule(css, callback)).toMatchInlineSnapshot(`".foo{color:red;}.bar{color:blue;}"`);
+
+    expect(callback).toHaveBeenCalledTimes(2);
+    expect(callback).toHaveBeenCalledWith(
+      expect.objectContaining({ type: RULESET, return: '.foo{color:red;}' }),
+      '.foo{color:red;}',
+    );
+    expect(callback).toHaveBeenCalledWith(
+      expect.objectContaining({ type: RULESET, return: '.bar{color:blue;}' }),
+      '.bar{color:blue;}',
+    );
+  });
+});
diff --git a/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.test.ts b/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.test.ts
index 1d197e7..c746639 100644
--- a/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.test.ts
+++ b/packages/core/src/runtime/stylis/sortClassesInAtRulesPlugin.test.ts
@@ -83,6 +83,26 @@ describe('sortClassesInAtRulesPlugin', () => {
         }
       `);
     });
+
+    it('handles @container', () => {
+      const css = `
+        @container utilities {
+          .b { padding-right: 1px; }
+          .a { padding-left: 1px; }
+        }
+      `;
+
+      expect(compileRule(css)).toMatchInlineSnapshot(`
+        @container utilities {
+          .a {
+            padding-left: 1px;
+          }
+          .b {
+            padding-right: 1px;
+          }
+        }
+      `);
+    });
   });
 
   it('handles nested at-rules', () => {