Repository: microsoft/griffel
Pull request #84 (2022-03-22 23:18:03)

feat: add support for @layer
This is part of my FHL project, so this is an exercise if nothing else. This PR adds support for @layer queries in griffel. How @layer works within an atomic CSS project is certainly a longer conversation. But here's the work to support it in a basic state. 

layershifter: @micahgodbolt there are some actions need for this PR 💡

- #52 introduced refactor to `resolveStyles`. While your PR does not have merge conflicts, fixes are needed.
  Just update `compileCSS()` calls in `packages/core/src/runtime/resolveStyleRules.ts` to include `layer`. 
- `layer` was included to hashing (in `bc764ce`) you need to update snapshots to use updated classes

👆 this should fix CI failures.
layershifter: @micahgodbolt I am entirely sure what we should do there, but let's put `@layer` also to separate style bucket.

#### `packages/core/src/runtime/getStyleBucketName.ts`

```diff
-export function getStyleBucketName(pseudo: string, media: string, support: string): StyleBucketName {
+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 || support) {
+ if (layer || media || support) {
```

#### `packages/core/src/runtime/getStyleBucketName.test.ts`

```diff
-    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(':active', '(max-width: 100px)', '')).toBe('t');
-    expect(getStyleBucketName(':active', '', '(display: table-cell)')).toBe('t');

+    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(':active', 'theme', '', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '', '(display: table-cell)')).toBe('t');
```

Calls of `getStyleBucketName` in `packages/core/src/runtime/resolveStyleRules.ts` should be also updated :)
layershifter: Non actionable:
We need to do better testing for hydration, but `@layer` is not supported in JSDOM (similar to https://github.com/jsdom/jsdom/issues/2026) 😿 
micahgodbolt: Do you need me to dig into the new bucket work? I'm on shield this week, so it might be a bit before I can dig in.
layershifter: > Do you need me to dig into the new bucket work? I'm on shield this week, so it might be a bit before I can dig in.

Please apply changes in https://github.com/microsoft/griffel/pull/84#issuecomment-1090133133 and we should be good to go 🐱 
micahgodbolt: > > Do you need me to dig into the new bucket work? I'm on shield this week, so it might be a bit before I can dig in.
> 
> Please apply changes in [#84 (comment)](https://github.com/microsoft/griffel/pull/84#issuecomment-1090133133) and we should be good to go 🐱

@layershifter are those suggestions the exact diffs you want to see made? Can you use the built in "add a suggestion" feature, and then I can just click to accept the change.
layershifter: > @layershifter are those suggestions the exact diffs you want to see made?

Yes

> Can you use the built in "add a suggestion" feature, and then I can just click to accept the change.

Unfortunately, these files are not present in PR I don't know how to create suggestions 🤔 
micahgodbolt: @layershifter thanks for pointing out the need to change getStyleBucketname. I've updated the function and tests
layershifter: > Calls of `getStyleBucketName` in `packages/core/src/runtime/resolveStyleRules.ts` should be also updated :)

@micahgodbolt I think that we are missing this step 👆 There are few similar failures:

```
../core/src/runtime/resolveStyleRules.ts:216:25 - error TS2345: Argument of type '{ value: string; property: string; pseudo: string; media: string; support: string; }' is not assignable to parameter of type 'HashedClassNameParts'.
  Property 'layer' is missing in type '{ value: string; property: string; pseudo: string; media: string; support: string; }' but required in type 'HashedClassNameParts'.

216         ? hashClassName({
                            ~
217             value: rtlDefinitions.map(v => (v?.value ?? '').toString()).join(';'),
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... 
221             support,
    ~~~~~~~~~~~~~~~~~~~~
222           })
    ~~~~~~~~~~~

  ../core/src/runtime/utils/hashClassName.ts:9:3
    9   layer: string;
        ~~~~~
    'layer' is declared here.

../core/src/runtime/resolveStyleRules.ts:233:31 - error TS2554: Expected 4 arguments, but got 3.

233       const styleBucketName = getStyleBucketName(pseudo, media, support);
```

Patch:
diff --git a/apps/website/docs/react/api/make-styles.md b/apps/website/docs/react/api/make-styles.md
index 5577977..f341ba6 100644
--- a/apps/website/docs/react/api/make-styles.md
+++ b/apps/website/docs/react/api/make-styles.md
@@ -122,7 +122,7 @@ html[data-whatintent='mouse'] .f1xz3i88 {
 
 ### At-rules
 
-`@media` & `@supports` queries are also supported:
+`@media`, `@supports` & `@layer` queries are also supported:
 
 ```js
 import { makeStyles } from '@griffel/react';
@@ -131,6 +131,7 @@ const useClasses = makeStyles({
   root: {
     '@media screen and (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
+    '@layer utility': { marginBottom: '1em' }
   },
 });
 ```
@@ -148,6 +149,12 @@ const useClasses = makeStyles({
     color: red;
   }
 }
+
+@layer utility {
+  .f2d3jla {
+    margin-bottom: 1em;
+  }
+}
 ```
 
 ### `@keyframes` (animations)
diff --git a/beachball.config.js b/beachball.config.js
index 1472bfb..cd0b916 100644
--- a/beachball.config.js
+++ b/beachball.config.js
@@ -5,5 +5,14 @@
  */
 module.exports = {
   gitTags: false,
+  ignorePatterns: [
+    '**/__fixtures__/**',
+    '**/*.test.{ts,tsx}',
+    '**/*.stories.tsx',
+    '**/.eslintrc.json',
+    '**/jest.config.js',
+    '**/project.json',
+    '**/README.md',
+  ],
   hooks: require('./beachball.hooks'),
 };
diff --git a/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json b/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json
new file mode 100644
index 0000000..28279be
--- /dev/null
+++ b/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: handle duplicated import sources properly",
+  "packageName": "@griffel/babel-preset",
+  "email": "rezha@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-babel-preset-430fbeb1-262d-49d0-b56d-1dbcf844d5f0.json b/change/@griffel-babel-preset-430fbeb1-262d-49d0-b56d-1dbcf844d5f0.json
new file mode 100644
index 0000000..e8aabf0
--- /dev/null
+++ b/change/@griffel-babel-preset-430fbeb1-262d-49d0-b56d-1dbcf844d5f0.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: add @fluentui/react-components to handled imports by default",
+  "packageName": "@griffel/babel-preset",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-babel-preset-9f9b43ef-1aa0-45c0-ba63-27be317357d3.json b/change/@griffel-babel-preset-9f9b43ef-1aa0-45c0-ba63-27be317357d3.json
new file mode 100644
index 0000000..c5bd823
--- /dev/null
+++ b/change/@griffel-babel-preset-9f9b43ef-1aa0-45c0-ba63-27be317357d3.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: re-export Module & EvalCache from Linaria",
+  "packageName": "@griffel/babel-preset",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json b/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json
new file mode 100644
index 0000000..f13a16d
--- /dev/null
+++ b/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json
@@ -0,0 +1,7 @@
+{
+  "type": "none",
+  "comment": "add a comment to exports",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "none"
+}
diff --git a/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json b/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json
new file mode 100644
index 0000000..27a4ba3
--- /dev/null
+++ b/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: add support for @layer",
+  "packageName": "@griffel/core",
+  "email": "mgodbolt@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json b/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json
new file mode 100644
index 0000000..4d9bbb5
--- /dev/null
+++ b/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "initial release",
+  "packageName": "@griffel/eslint-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json b/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json
new file mode 100644
index 0000000..02b672d
--- /dev/null
+++ b/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json
@@ -0,0 +1,7 @@
+{
+  "type": "none",
+  "comment": "docs: add support for @layer",
+  "packageName": "@griffel/react",
+  "email": "mgodbolt@microsoft.com",
+  "dependentChangeType": "none"
+}
diff --git a/change/@griffel-webpack-loader-6a8b470c-f154-43cc-aebb-08c5a47e124d.json b/change/@griffel-webpack-loader-6a8b470c-f154-43cc-aebb-08c5a47e124d.json
new file mode 100644
index 0000000..b253b34
--- /dev/null
+++ b/change/@griffel-webpack-loader-6a8b470c-f154-43cc-aebb-08c5a47e124d.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: remove dependency on @linaria/babel-preset",
+  "packageName": "@griffel/webpack-loader",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/package.json b/package.json
index 71d985e..0cfdb04 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
     "@emotion/hash": "^0.8.0",
     "@linaria/babel-preset": "^3.0.0-beta.14",
     "@linaria/shaker": "^3.0.0-beta.14",
+    "@typescript-eslint/utils": "^5.20.0",
     "ajv": "^8.4.0",
     "csstype": "^3.0.10",
     "enhanced-resolve": "^5.8.2",
diff --git a/packages/babel-preset/README.md b/packages/babel-preset/README.md
index ee1f386..88652bb 100644
--- a/packages/babel-preset/README.md
+++ b/packages/babel-preset/README.md
@@ -33,7 +33,7 @@ import { makeStyles } from 'custom-package';
 import { createStyles } from 'custom-package';
 ```
 
-By default, preset handles imports from `@griffel/react`, to handle imports from custom packages settings should be tweaked:
+By default, preset handles imports from `@griffel/react` & `@fluentui/react-components`, to handle imports from custom packages settings should be tweaked:
 
 ```json
 {
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/code.ts b/packages/babel-preset/__fixtures__/duplicated-imports/code.ts
new file mode 100644
index 0000000..8c4eb21
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/code.ts
@@ -0,0 +1,12 @@
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { createStylesA } from 'custom-package';
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { createStylesB } from 'custom-package';
+
+export const useClassesA = createStylesA({
+  root: { color: 'red' },
+});
+
+export const useClassesB = createStylesB({
+  root: { color: 'yellow' },
+});
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/options.json b/packages/babel-preset/__fixtures__/duplicated-imports/options.json
new file mode 100644
index 0000000..d8906ae
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/options.json
@@ -0,0 +1,3 @@
+{
+  "modules": [{ "moduleSource": "custom-package", "importName": "createStylesA" }]
+}
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/output.ts b/packages/babel-preset/__fixtures__/duplicated-imports/output.ts
new file mode 100644
index 0000000..16a685f
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/output.ts
@@ -0,0 +1,19 @@
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { __styles } from 'custom-package'; // @ts-expect-error This module does not exist, but will be resolved via aliases
+
+import { createStylesB } from 'custom-package';
+export const useClassesA = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+    },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
+export const useClassesB = createStylesB({
+  root: {
+    color: 'yellow',
+  },
+});
diff --git a/packages/babel-preset/__fixtures__/require-custom-module/code.ts b/packages/babel-preset/__fixtures__/require-custom-module/code.ts
new file mode 100644
index 0000000..21ca02b
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/require-custom-module/code.ts
@@ -0,0 +1,10 @@
+const react_make_styles_1 = require('custom-package');
+
+export const useStyles = react_make_styles_1.makeStyles({
+  root: {
+    fontSize: '14px',
+    lineHeight: 1,
+  },
+});
+
+console.log(useStyles);
diff --git a/packages/babel-preset/__fixtures__/require-custom-module/options.json b/packages/babel-preset/__fixtures__/require-custom-module/options.json
new file mode 100644
index 0000000..4d20ffe
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/require-custom-module/options.json
@@ -0,0 +1,3 @@
+{
+  "modules": [{ "moduleSource": "custom-package", "importName": "makeStyles" }]
+}
diff --git a/packages/babel-preset/__fixtures__/require-custom-module/output.ts b/packages/babel-preset/__fixtures__/require-custom-module/output.ts
new file mode 100644
index 0000000..33a609b
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/require-custom-module/output.ts
@@ -0,0 +1,14 @@
+const react_make_styles_1 = require('custom-package');
+
+export const useStyles = react_make_styles_1.__styles(
+  {
+    root: {
+      Be2twd7: 'fses1vf',
+      Bg96gwp: 'fp6vxd',
+    },
+  },
+  {
+    d: ['.fses1vf{font-size:14px;}', '.fp6vxd{line-height:1;}'],
+  },
+);
+console.log(useStyles);
diff --git a/packages/babel-preset/__fixtures__/commonjs/code.ts b/packages/babel-preset/__fixtures__/require/code.ts
similarity index 100%
rename from packages/babel-preset/__fixtures__/commonjs/code.ts
rename to packages/babel-preset/__fixtures__/require/code.ts
diff --git a/packages/babel-preset/__fixtures__/commonjs/output.ts b/packages/babel-preset/__fixtures__/require/output.ts
similarity index 100%
rename from packages/babel-preset/__fixtures__/commonjs/output.ts
rename to packages/babel-preset/__fixtures__/require/output.ts
diff --git a/packages/babel-preset/src/index.ts b/packages/babel-preset/src/index.ts
index 0342daf..d6f2dd9 100644
--- a/packages/babel-preset/src/index.ts
+++ b/packages/babel-preset/src/index.ts
@@ -4,6 +4,7 @@ import type { ConfigAPI } from '@babel/core';
 import type { BabelPluginOptions } from './types';
 
 export { default as shakerEvaluator } from '@linaria/shaker';
+export { EvalCache, Module } from '@linaria/babel-preset';
 export { configSchema } from './schema';
 
 export type { Evaluator, EvalRule } from '@linaria/babel-preset';
diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts
index 75e86b3..bdf3dae 100644
--- a/packages/babel-preset/src/transformPlugin.ts
+++ b/packages/babel-preset/src/transformPlugin.ts
@@ -10,7 +10,7 @@ import { BabelPluginOptions } from './types';
 import { validateOptions } from './validateOptions';
 
 type BabelPluginState = PluginPass & {
-  importDeclarationPath?: NodePath<t.ImportDeclaration>;
+  importDeclarationPaths?: NodePath<t.ImportDeclaration>[];
   requireDeclarationPath?: NodePath<t.VariableDeclarator>;
 
   definitionPaths?: NodePath<t.ObjectExpression>[];
@@ -65,7 +65,10 @@ function hasMakeStylesImport(
  *
  * @example react_make_styles_1 = require('@griffel/react')
  */
-function isRequireDeclarator(path: NodePath<t.VariableDeclarator>): boolean {
+function isRequireDeclarator(
+  path: NodePath<t.VariableDeclarator>,
+  modules: NonNullable<BabelPluginOptions['modules']>,
+): boolean {
   const initPath = path.get('init');
 
   if (!initPath.isCallExpression()) {
@@ -75,7 +78,13 @@ function isRequireDeclarator(path: NodePath<t.VariableDeclarator>): boolean {
   if (initPath.get('callee').isIdentifier({ name: 'require' })) {
     const args = initPath.get('arguments');
 
-    return Array.isArray(args) && args.length === 1 && args[0].isStringLiteral({ value: '@griffel/react' });
+    if (Array.isArray(args) && args.length === 1) {
+      const moduleNamePath = args[0];
+
+      if (moduleNamePath.isStringLiteral()) {
+        return Boolean(modules.find(module => moduleNamePath.node.value === module.moduleSource));
+      }
+    }
   }
 
   return false;
@@ -100,7 +109,10 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
 
   const pluginOptions: Required<BabelPluginOptions> = {
     babelOptions: {},
-    modules: [{ moduleSource: '@griffel/react', importName: 'makeStyles' }],
+    modules: [
+      { moduleSource: '@griffel/react', importName: 'makeStyles' },
+      { moduleSource: '@fluentui/react-components', importName: 'makeStyles' },
+    ],
     evaluationRules: [
       { action: shakerEvaluator },
       {
@@ -118,6 +130,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
     name: '@griffel/babel-plugin-transform',
 
     pre() {
+      this.importDeclarationPaths = [];
       this.definitionPaths = [];
       this.calleePaths = [];
     },
@@ -130,7 +143,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
         },
 
         exit(path, state) {
-          if (!state.importDeclarationPath && !state.requireDeclarationPath) {
+          if (state.importDeclarationPaths!.length === 0 && !state.requireDeclarationPath) {
             return;
           }
 
@@ -160,9 +173,9 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
             });
           }
 
-          if (state.importDeclarationPath) {
-            const specifiers = state.importDeclarationPath.get('specifiers');
-            const source = state.importDeclarationPath.get('source');
+          state.importDeclarationPaths!.forEach(importDeclarationPath => {
+            const specifiers = importDeclarationPath.get('specifiers');
+            const source = importDeclarationPath.get('source');
 
             specifiers.forEach(specifier => {
               if (specifier.isImportSpecifier()) {
@@ -182,7 +195,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
                 }
               }
             });
-          }
+          });
 
           if (state.calleePaths) {
             state.calleePaths.forEach(calleePath => {
@@ -195,13 +208,13 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
       // eslint-disable-next-line @typescript-eslint/naming-convention
       ImportDeclaration(path, state) {
         if (hasMakeStylesImport(path, pluginOptions.modules)) {
-          state.importDeclarationPath = path;
+          state.importDeclarationPaths!.push(path);
         }
       },
 
       // eslint-disable-next-line @typescript-eslint/naming-convention
       VariableDeclarator(path, state) {
-        if (isRequireDeclarator(path)) {
+        if (isRequireDeclarator(path, pluginOptions.modules)) {
           state.requireDeclarationPath = path;
         }
       },
@@ -213,7 +226,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
          *
          * @example makeStyles({})
          */
-        if (!state.importDeclarationPath) {
+        if (state.importDeclarationPaths!.length === 0) {
           return;
         }
 
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index bd186d1..0278a1b 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -65,4 +65,5 @@ export type {
   GriffelRenderer,
 } from './types';
 
+// Private exports, are used by devtools
 export type { DebugCSSRules, DebugSequence, DebugResult } from './devtools';
diff --git a/packages/core/src/renderer/rehydrateRendererCache.ts b/packages/core/src/renderer/rehydrateRendererCache.ts
index 9dd34f5..5ba2986 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.ts
@@ -3,7 +3,7 @@ import { GriffelRenderer, StyleBucketName } from '../types';
 // Regexps to extract names of classes and animations
 // https://github.com/styletron/styletron/blob/e0fcae826744eb00ce679ac613a1b10d44256660/packages/styletron-engine-atomic/src/client/client.js#L8
 const KEYFRAMES_HYDRATOR = /@(-webkit-)?keyframes ([^{]+){((?:(?:from|to|(?:\d+\.?\d*%))\{(?:[^}])*})*)}/g;
-const AT_RULES_HYDRATOR = /@(media|supports)[^{]+\{([\s\S]+?})\s*}/g;
+const AT_RULES_HYDRATOR = /@(media|supports|layer)[^{]+\{([\s\S]+?})\s*}/g;
 const STYLES_HYDRATOR = /\.([^{:]+)(:[^{]+)?{(?:[^}]*;)?([^}]*?)}/g;
 
 const regexps: Partial<Record<StyleBucketName, RegExp>> = {
diff --git a/packages/core/src/runtime/compileCSS.ts b/packages/core/src/runtime/compileCSS.ts
index 8dfcb5b..11adaeb 100644
--- a/packages/core/src/runtime/compileCSS.ts
+++ b/packages/core/src/runtime/compileCSS.ts
@@ -8,6 +8,7 @@ export interface CompileCSSOptions {
 
   pseudo: string;
   media: string;
+  layer: string;
   support: string;
 
   property: string;
@@ -60,7 +61,7 @@ export function compileCSSRules(cssRules: string): string[] {
 }
 
 export function compileCSS(options: CompileCSSOptions): [string /* ltr definition */, string? /* rtl definition */] {
-  const { className, media, pseudo, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
+  const { className, media, layer, pseudo, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
 
   const classNameSelector = `.${className}`;
   const cssDeclaration = Array.isArray(value)
@@ -109,6 +110,10 @@ export function compileCSS(options: CompileCSSOptions): [string /* ltr definitio
     cssRule = `@media ${media} { ${cssRule} }`;
   }
 
+  if (layer) {
+    cssRule = `@layer ${layer} { ${cssRule} }`;
+  }
+
   if (support) {
     cssRule = `@supports ${support} { ${cssRule} }`;
   }
diff --git a/packages/core/src/runtime/getStyleBucketName.ts b/packages/core/src/runtime/getStyleBucketName.ts
index ac4bfa3..1bab581 100644
--- a/packages/core/src/runtime/getStyleBucketName.ts
+++ b/packages/core/src/runtime/getStyleBucketName.ts
@@ -40,9 +40,9 @@ const pseudosMap: Record<string, StyleBucketName | undefined> = {
  * "f"
  * ```
  */
-export function getStyleBucketName(pseudo: string, media: string, support: string): StyleBucketName {
+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 || support) {
+  if (media || layer || support) {
     return 't';
   }
 
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index 588c27c..6a368db 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -7,6 +7,7 @@ import { compileCSS, CompileCSSOptions } from './compileCSS';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
 import { generateCombinedQuery } from './utils/generateCombinedMediaQuery';
 import { isMediaQuerySelector } from './utils/isMediaQuerySelector';
+import { isLayerSelector } from './utils/isLayerSelector';
 import { isNestedSelector } from './utils/isNestedSelector';
 import { isSupportQuerySelector } from './utils/isSupportQuerySelector';
 import { normalizeNestedProperty } from './utils/normalizeNestedProperty';
@@ -48,6 +49,7 @@ export function resolveStyleRules(
   styles: GriffelStyle,
   pseudo = '',
   media = '',
+  layer = '',
   support = '',
   cssClassesMap: CSSClassesMap = {},
   cssRulesByBucket: CSSRulesByBucket = {},
@@ -85,6 +87,7 @@ export function resolveStyleRules(
       const key = hashPropertyKey(pseudo, media, support, property);
       const className = hashClassName({
         media,
+        layer,
         value: value.toString(),
         support,
         pseudo,
@@ -100,6 +103,7 @@ export function resolveStyleRules(
             property: rtlDefinition.key,
             pseudo,
             media,
+            layer,
             support,
           })
         : undefined;
@@ -111,10 +115,11 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(pseudo, media, support);
+      const styleBucketName = getStyleBucketName(pseudo, layer, media, support);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
+        layer,
         pseudo,
         property,
         support,
@@ -166,6 +171,7 @@ export function resolveStyleRules(
         { animationName: animationNames.join(', ') },
         pseudo,
         media,
+        layer,
         support,
         cssClassesMap,
         cssRulesByBucket,
@@ -185,6 +191,7 @@ export function resolveStyleRules(
       const key = hashPropertyKey(pseudo, media, support, property);
       const className = hashClassName({
         media,
+        layer,
         value: value.map(v => (v ?? '').toString()).join(';'),
         support,
         pseudo,
@@ -211,6 +218,7 @@ export function resolveStyleRules(
             value: rtlDefinitions.map(v => (v?.value ?? '').toString()).join(';'),
             property: rtlDefinitions[0].key,
             pseudo,
+            layer,
             media,
             support,
           })
@@ -224,10 +232,11 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(pseudo, media, support);
+      const styleBucketName = getStyleBucketName(pseudo, layer, media, support);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
+        layer,
         pseudo,
         property,
         support,
@@ -243,6 +252,7 @@ export function resolveStyleRules(
           value as GriffelStyle,
           pseudo + normalizeNestedProperty(property),
           media,
+          layer,
           support,
           cssClassesMap,
           cssRulesByBucket,
@@ -250,11 +260,39 @@ export function resolveStyleRules(
       } else if (isMediaQuerySelector(property)) {
         const combinedMediaQuery = generateCombinedQuery(media, property.slice(6).trim());
 
-        resolveStyleRules(value as GriffelStyle, pseudo, combinedMediaQuery, support, cssClassesMap, cssRulesByBucket);
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          combinedMediaQuery,
+          layer,
+          support,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
+      } else if (isLayerSelector(property)) {
+        const combinedLayerQuery = (layer ? `${layer}.` : '') + property.slice(6).trim();
+
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          media,
+          combinedLayerQuery,
+          support,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
       } else if (isSupportQuerySelector(property)) {
         const combinedSupportQuery = generateCombinedQuery(support, property.slice(9).trim());
 
-        resolveStyleRules(value as GriffelStyle, pseudo, media, combinedSupportQuery, cssClassesMap, cssRulesByBucket);
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          media,
+          layer,
+          combinedSupportQuery,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
       } else {
         if (process.env.NODE_ENV !== 'production') {
           // eslint-disable-next-line no-console
diff --git a/packages/core/src/runtime/utils/hashClassName.ts b/packages/core/src/runtime/utils/hashClassName.ts
index c2cf369..70a602a 100644
--- a/packages/core/src/runtime/utils/hashClassName.ts
+++ b/packages/core/src/runtime/utils/hashClassName.ts
@@ -6,12 +6,13 @@ export interface HashedClassNameParts {
   value: string;
   pseudo: string;
   media: string;
+  layer: string;
   support: string;
 }
 
-export function hashClassName({ media, property, pseudo, support, value }: HashedClassNameParts): string {
+export function hashClassName({ media, layer, property, pseudo, support, value }: HashedClassNameParts): string {
   // Trimming of value is required to generate consistent hashes
-  const classNameHash = hashString(pseudo + media + support + property + value.trim());
+  const classNameHash = hashString(pseudo + media + layer + support + property + value.trim());
 
   return HASH_PREFIX + classNameHash;
 }
diff --git a/packages/core/src/runtime/utils/isLayerSelector.ts b/packages/core/src/runtime/utils/isLayerSelector.ts
new file mode 100644
index 0000000..85c531a
--- /dev/null
+++ b/packages/core/src/runtime/utils/isLayerSelector.ts
@@ -0,0 +1,3 @@
+export function isLayerSelector(property: string): boolean {
+  return property.substr(0, 6) === '@layer';
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 401a054..ec71957 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -126,7 +126,7 @@ export type StyleBucketName =
   | 'a'
   // @keyframes definitions
   | 'k'
-  // at-rules (@media, @support)
+  // at-rules (@media, @support, @layer)
   | 't';
 
 export type SequenceHash = string;
diff --git a/packages/devtools/.storybook/main.js b/packages/devtools/.storybook/main.js
new file mode 100644
index 0000000..2abab50
--- /dev/null
+++ b/packages/devtools/.storybook/main.js
@@ -0,0 +1,21 @@
+const rootMain = require('../../../.storybook/main');
+
+module.exports = {
+  ...rootMain,
+
+  core: { ...rootMain.core, builder: 'webpack5' },
+
+  stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+  addons: [...rootMain.addons, '@nrwl/react/plugins/storybook'],
+
+  webpackFinal: async (config, { configType }) => {
+    // apply any global webpack configs that might have been specified in .storybook/main.js
+    if (rootMain.webpackFinal) {
+      config = await rootMain.webpackFinal(config, { configType });
+    }
+
+    // add your own webpack tweaks if needed
+
+    return config;
+  },
+};
diff --git a/packages/devtools/.storybook/preview-head.html b/packages/devtools/.storybook/preview-head.html
new file mode 100644
index 0000000..b515033
--- /dev/null
+++ b/packages/devtools/.storybook/preview-head.html
@@ -0,0 +1,10 @@
+<style>
+  html,
+  body {
+    font-size: 14px;
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+  }
+</style>
diff --git a/packages/devtools/.storybook/tsconfig.json b/packages/devtools/.storybook/tsconfig.json
new file mode 100644
index 0000000..e3aaeea
--- /dev/null
+++ b/packages/devtools/.storybook/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "emitDecoratorMetadata": true,
+    "noEmit": true,
+    "outDir": ""
+  },
+  "files": [
+    "../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
+    "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+    "../../../node_modules/@nrwl/react/typings/image.d.ts"
+  ],
+  "exclude": ["../**/*.spec.ts", "../**/*.spec.js", "../**/*.spec.tsx", "../**/*.spec.jsx"],
+  "include": ["../src/**/*", "*.js"]
+}
diff --git a/packages/devtools/README.md b/packages/devtools/README.md
index d8fa758..f67a99c 100644
--- a/packages/devtools/README.md
+++ b/packages/devtools/README.md
@@ -7,3 +7,7 @@ Chrome devtools panel extension for debugging griffel.
 - run `yarn nx run @griffel/devtools:watch` from workspace root
 - load extension from _dist/packages/devtools_ folder
 - open chrome devtools, inspect extension in devtools panel (next to 'Styles' tab).
+
+### Storybook
+
+Start storybook by `yarn nx run @griffel/devtools:storybook` and inspect devtools components
diff --git a/packages/devtools/project.json b/packages/devtools/project.json
index 26ad67e..168b808 100644
--- a/packages/devtools/project.json
+++ b/packages/devtools/project.json
@@ -82,6 +82,37 @@
         "parallel": false,
         "commands": [{ "command": " nx build @griffel/devtools --skip-nx-cache --configuration=development --watch" }]
       }
+    },
+    "storybook": {
+      "executor": "@nrwl/storybook:storybook",
+      "options": {
+        "uiFramework": "@storybook/react",
+        "port": 4400,
+        "config": {
+          "configFolder": "packages/devtools/.storybook"
+        }
+      },
+      "configurations": {
+        "ci": {
+          "quiet": true
+        }
+      }
+    },
+    "build-storybook": {
+      "executor": "@nrwl/storybook:build",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "uiFramework": "@storybook/react",
+        "outputPath": "dist/storybook/@griffel/devtools",
+        "config": {
+          "configFolder": "packages/devtools/.storybook"
+        }
+      },
+      "configurations": {
+        "ci": {
+          "quiet": true
+        }
+      }
     }
   }
 }
diff --git a/packages/devtools/src/index.html b/packages/devtools/src/index.html
index 26cc85f..2d1e7de 100644
--- a/packages/devtools/src/index.html
+++ b/packages/devtools/src/index.html
@@ -3,6 +3,17 @@
   <head>
     <meta charset="utf-8" />
     <title>DevTools for Griffel</title>
+
+    <style>
+      html,
+      body {
+        font-size: 14px;
+        width: 100%;
+        height: 100%;
+        padding: 0;
+        margin: 0;
+      }
+    </style>
   </head>
   <body>
     <div id="root"></div>
diff --git a/packages/devtools/src/stories/DefaultMessage.stories.tsx b/packages/devtools/src/stories/DefaultMessage.stories.tsx
new file mode 100644
index 0000000..779b6be
--- /dev/null
+++ b/packages/devtools/src/stories/DefaultMessage.stories.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { DefaultMessage } from '../DefaultMessage';
+
+export const Default = () => (
+  <div style={{ display: 'flex', gap: 10, flexDirection: 'column' }}>
+    <div style={{ border: '3px solid gray', width: 300 }}>
+      <DefaultMessage />
+    </div>
+    <div style={{ border: '3px solid gray', width: 600 }}>
+      <DefaultMessage />
+    </div>
+  </div>
+);
+
+export default {
+  title: 'DefaultMessage',
+};
diff --git a/packages/devtools/tsconfig.json b/packages/devtools/tsconfig.json
index 4c08958..c63e275 100644
--- a/packages/devtools/tsconfig.json
+++ b/packages/devtools/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "jsx": "react-jsx",
+    "jsx": "react",
     "allowJs": true,
     "esModuleInterop": true,
     "allowSyntheticDefaultImports": true,
@@ -20,6 +20,9 @@
     },
     {
       "path": "./tsconfig.spec.json"
+    },
+    {
+      "path": "./.storybook/tsconfig.json"
     }
   ]
 }
diff --git a/packages/devtools/tsconfig.lib.json b/packages/devtools/tsconfig.lib.json
index c39304d..4e297c0 100644
--- a/packages/devtools/tsconfig.lib.json
+++ b/packages/devtools/tsconfig.lib.json
@@ -12,7 +12,15 @@
     "**/*.spec.js",
     "**/*.test.js",
     "**/*.spec.jsx",
-    "**/*.test.jsx"
+    "**/*.test.jsx",
+    "**/*.stories.ts",
+    "**/*.stories.js",
+    "**/*.stories.jsx",
+    "**/*.stories.tsx",
+    "**/*.stories.ts",
+    "**/*.stories.js",
+    "**/*.stories.jsx",
+    "**/*.stories.tsx"
   ],
   "include": ["**/*.ts", "**/*.tsx"]
 }
diff --git a/packages/eslint-plugin/.babelrc b/packages/eslint-plugin/.babelrc
new file mode 100644
index 0000000..cf7ddd9
--- /dev/null
+++ b/packages/eslint-plugin/.babelrc
@@ -0,0 +1,3 @@
+{
+  "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
+}
diff --git a/packages/eslint-plugin/.eslintrc.json b/packages/eslint-plugin/.eslintrc.json
new file mode 100644
index 0000000..9d9c0db
--- /dev/null
+++ b/packages/eslint-plugin/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
new file mode 100644
index 0000000..f353207
--- /dev/null
+++ b/packages/eslint-plugin/README.md
@@ -0,0 +1,44 @@
+# ESLint plugin for Griffel
+
+ESLint plugin to follow best practices and anticipate common mistakes when writing styles with Griffel
+
+## Install
+
+```bash
+yarn add --dev @griffel/eslint-plugin
+# or
+npm install --dev @griffel/eslint-plugin
+```
+
+## Usage
+
+Add `@griffel/eslint-plugin` to the extends section of your `.eslintrc` configuration file:
+
+```json
+{
+  "extends": ["@griffel"]
+}
+```
+
+## Shareable configurations
+
+This plugin exports recommended configuration that enforce good practices, but you can use only uses that are required for your use case:
+
+```json
+{
+  "plugins": ["@griffel"],
+  "rules": {
+    "@griffel/no-shorthands": "warn"
+  }
+}
+```
+
+You can find more info about enabled rules in the [Supported Rules section](#supported-rules) below.
+
+## Supported Rules
+
+**Key**: 🔧 = fixable
+
+| Name                                                     | Description                    | 🔧  |
+| -------------------------------------------------------- | ------------------------------ | --- |
+| [`@griffel/no-shorthands`](./src/rules/no-shorthands.md) | Enforce usage of CSS longhands |     |
diff --git a/packages/eslint-plugin/jest.config.js b/packages/eslint-plugin/jest.config.js
new file mode 100644
index 0000000..81437d4
--- /dev/null
+++ b/packages/eslint-plugin/jest.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+  displayName: 'eslint-plugin',
+  preset: '../../jest.preset.js',
+  globals: {
+    'ts-jest': {
+      tsconfig: '<rootDir>/tsconfig.spec.json',
+    },
+  },
+  testEnvironment: 'node',
+  transform: {
+    '^.+\\.[tj]sx?$': 'ts-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../coverage/packages/eslint-plugin',
+};
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
new file mode 100644
index 0000000..c030a69
--- /dev/null
+++ b/packages/eslint-plugin/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "@griffel/eslint-plugin",
+  "version": "0.0.1",
+  "description": "ESLint plugin with lint rules for Griffel",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/microsoft/griffel"
+  },
+  "dependencies": {
+    "csstype": "^3.0.10",
+    "@typescript-eslint/utils": "^5.20.0"
+  },
+  "peerDependencies": {
+    "eslint": "^7.5.0 || ^8.0.0"
+  }
+}
diff --git a/packages/eslint-plugin/project.json b/packages/eslint-plugin/project.json
new file mode 100644
index 0000000..f64f373
--- /dev/null
+++ b/packages/eslint-plugin/project.json
@@ -0,0 +1,49 @@
+{
+  "root": "packages/eslint-plugin",
+  "sourceRoot": "packages/eslint-plugin/src",
+  "projectType": "library",
+  "targets": {
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["packages/eslint-plugin/**/*.ts"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/packages/eslint-plugin"],
+      "options": {
+        "jestConfig": "packages/eslint-plugin/jest.config.js",
+        "passWithNoTests": true
+      }
+    },
+    "build": {
+      "executor": "@nrwl/node:package",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/packages/eslint-plugin",
+        "tsConfig": "packages/eslint-plugin/tsconfig.lib.json",
+        "packageJson": "packages/eslint-plugin/package.json",
+        "main": "packages/eslint-plugin/src/index.ts",
+        "assets": [
+          "packages/eslint-plugin/README.md",
+          {
+            "glob": "LICENSE.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "type-check": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "packages/babel-preset",
+        "commands": [{ "command": "tsc -b --pretty" }],
+        "outputPath": []
+      }
+    }
+  },
+  "tags": []
+}
diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts
new file mode 100644
index 0000000..5691dc4
--- /dev/null
+++ b/packages/eslint-plugin/src/configs/recommended.ts
@@ -0,0 +1,6 @@
+export const recommendedConfig = {
+  plugins: ['@griffel'],
+  rules: {
+    '@griffel/no-shorthands': 'error',
+  },
+};
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
new file mode 100644
index 0000000..7bf7b16
--- /dev/null
+++ b/packages/eslint-plugin/src/index.ts
@@ -0,0 +1,11 @@
+import { recommendedConfig } from './configs/recommended';
+import { noShorthandsRule } from './rules/no-shorthands';
+
+export = {
+  configs: {
+    recommended: recommendedConfig,
+  },
+  rules: {
+    'no-shorthands': noShorthandsRule,
+  },
+};
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.md b/packages/eslint-plugin/src/rules/no-shorthands.md
new file mode 100644
index 0000000..89b2485
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.md
@@ -0,0 +1,34 @@
+# Enforce usage of CSS longhands (`@griffel/no-shorthands`)
+
+Ensure that rules passed to `makeStyles()` function do not contain [CSS shorthands](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties).
+
+## Rule Details
+
+Griffel does not support usage of CSS shorthands, [microsoft/griffel#30](https://github.com/microsoft/griffel/issues/30#issuecomment-1045959297).
+This is enforced by typings, but there are cases when they don't work as expected [microsoft/griffel#78](https://github.com/microsoft/griffel/issues/78).
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    background: 'red',
+    padding: '10px 20px',
+  },
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    backgroundColor: 'red',
+    ...shorthands.padding('10px', '20px'),
+  },
+});
+```
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.ts b/packages/eslint-plugin/src/rules/no-shorthands.ts
new file mode 100644
index 0000000..ed1b1bc
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.ts
@@ -0,0 +1,122 @@
+import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
+import * as CSS from 'csstype';
+
+import { createRule } from '../utils/createRule';
+import { isIdentifier, isObjectExpression, isProperty } from '../utils/helpers';
+
+export const RULE_NAME = 'no-shorthands';
+
+// This collection is a map simply for faster access when checking if a CSS property is unsupported
+// @griffel/core has the same definition, but ESLint plugin should not depend on it
+const UNSUPPORTED_CSS_PROPERTIES: Record<keyof CSS.StandardShorthandProperties, true> = {
+  all: true,
+  animation: true,
+  background: true,
+  backgroundPosition: true,
+  border: true,
+  borderBlock: true,
+  borderBlockEnd: true,
+  borderBlockStart: true,
+  borderBottom: true,
+  borderColor: true,
+  borderImage: true,
+  borderInline: true,
+  borderInlineEnd: true,
+  borderInlineStart: true,
+  borderLeft: true,
+  borderRadius: true,
+  borderRight: true,
+  borderStyle: true,
+  borderTop: true,
+  borderWidth: true,
+  columns: true,
+  columnRule: true,
+  flex: true,
+  flexFlow: true,
+  font: true,
+  gap: true,
+  grid: true,
+  gridArea: true,
+  gridColumn: true,
+  gridRow: true,
+  gridTemplate: true,
+  lineClamp: true,
+  listStyle: true,
+  margin: true,
+  mask: true,
+  maskBorder: true,
+  motion: true,
+  offset: true,
+  outline: true,
+  overflow: true,
+  overscrollBehavior: true,
+  padding: true,
+  placeItems: true,
+  placeSelf: true,
+  textDecoration: true,
+  textEmphasis: true,
+  transition: true,
+};
+
+function findShorthandProperties(
+  node: TSESTree.ObjectExpression,
+  isRoot = false,
+  result: TSESTree.Identifier[] = [],
+): TSESTree.Identifier[] {
+  for (const propertyNode of node.properties) {
+    if (isProperty(propertyNode)) {
+      if (isIdentifier(propertyNode.key) && !isRoot) {
+        if (Object.prototype.hasOwnProperty.call(UNSUPPORTED_CSS_PROPERTIES, propertyNode.key.name)) {
+          result.push(propertyNode.key);
+        }
+      }
+
+      if (isObjectExpression(propertyNode.value)) {
+        findShorthandProperties(propertyNode.value, false, result);
+      }
+    }
+  }
+
+  return result;
+}
+
+export const noShorthandsRule: ReturnType<ReturnType<typeof ESLintUtils.RuleCreator>> = createRule({
+  name: RULE_NAME,
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Disallow using CSS shorthands in makeStyles() calls',
+      recommended: 'error',
+    },
+    messages: {
+      shorthandFound: 'Usage of shorthands is prohibited',
+    },
+    schema: [
+      {
+        type: 'string',
+      },
+    ],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      CallExpression(node) {
+        if (isIdentifier(node.callee) && node.callee.name === 'makeStyles') {
+          const argument = node.arguments[0];
+
+          if (isObjectExpression(argument)) {
+            const shorthandProperties = findShorthandProperties(argument, true);
+
+            shorthandProperties.forEach(shorthandProperty => {
+              context.report({
+                node: shorthandProperty,
+                messageId: 'shorthandFound',
+              });
+            });
+          }
+        }
+      },
+    };
+  },
+});
diff --git a/packages/eslint-plugin/src/utils/createRule.ts b/packages/eslint-plugin/src/utils/createRule.ts
new file mode 100644
index 0000000..8614b4b
--- /dev/null
+++ b/packages/eslint-plugin/src/utils/createRule.ts
@@ -0,0 +1,7 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+const getDocsUrl = (ruleName: string): string =>
+  `https://github.com/microsoft/griffel/tree/main/packages/eslint-plugin/src/rules/${ruleName}.md`;
+
+export const createRule: ReturnType<typeof ESLintUtils.RuleCreator> = (...args) =>
+  ESLintUtils.RuleCreator(getDocsUrl)(...args);
diff --git a/packages/eslint-plugin/src/utils/helpers.ts b/packages/eslint-plugin/src/utils/helpers.ts
new file mode 100644
index 0000000..64babcb
--- /dev/null
+++ b/packages/eslint-plugin/src/utils/helpers.ts
@@ -0,0 +1,11 @@
+import { AST_NODE_TYPES, ASTUtils, TSESTree } from '@typescript-eslint/utils';
+
+type IsHelper<NodeType extends AST_NODE_TYPES> = (node: TSESTree.Node | null | undefined) => node is TSESTree.Node & {
+  type: NodeType;
+};
+
+export const isIdentifier: IsHelper<AST_NODE_TYPES.Identifier> = ASTUtils.isIdentifier;
+export const isObjectExpression: IsHelper<AST_NODE_TYPES.ObjectExpression> = ASTUtils.isNodeOfType(
+  AST_NODE_TYPES.ObjectExpression,
+);
+export const isProperty: IsHelper<AST_NODE_TYPES.Property> = ASTUtils.isNodeOfType(AST_NODE_TYPES.Property);
diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json
new file mode 100644
index 0000000..e464c9e
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/packages/eslint-plugin/tsconfig.lib.json b/packages/eslint-plugin/tsconfig.lib.json
new file mode 100644
index 0000000..8d27d94
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.lib.json
@@ -0,0 +1,11 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "module": "commonjs",
+    "outDir": "../../dist/out-tsc",
+    "declaration": true,
+    "types": ["node", "environment"]
+  },
+  "exclude": ["**/*.spec.ts", "**/*.test.ts"],
+  "include": ["**/*.ts"]
+}
diff --git a/packages/eslint-plugin/tsconfig.spec.json b/packages/eslint-plugin/tsconfig.spec.json
new file mode 100644
index 0000000..25c45ce
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node", "environment"]
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/packages/react/README.md b/packages/react/README.md
index b7534c3..8f1d3db 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -187,6 +187,7 @@ const useClasses = makeStyles({
 
     '@media screen and (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
+    '@layer utility': { marginBottom: '1em' }
   },
 });
 ```
diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json
index b72eb32..94089e0 100644
--- a/packages/webpack-loader/package.json
+++ b/packages/webpack-loader/package.json
@@ -10,7 +10,6 @@
   "dependencies": {
     "@babel/core": "^7.12.13",
     "@griffel/babel-preset": "^1.2.0",
-    "@linaria/babel-preset": "^3.0.0-beta.14",
     "enhanced-resolve": "^5.8.2",
     "loader-utils": "^2.0.0",
     "schema-utils": "^3.1.1",
diff --git a/packages/webpack-loader/src/webpackLoader.ts b/packages/webpack-loader/src/webpackLoader.ts
index 11388c7..08bccd1 100644
--- a/packages/webpack-loader/src/webpackLoader.ts
+++ b/packages/webpack-loader/src/webpackLoader.ts
@@ -1,5 +1,4 @@
-import { configSchema, BabelPluginOptions } from '@griffel/babel-preset';
-import { EvalCache, Module } from '@linaria/babel-preset';
+import { configSchema, BabelPluginOptions, EvalCache, Module } from '@griffel/babel-preset';
 import * as enhancedResolve from 'enhanced-resolve';
 import { getOptions } from 'loader-utils';
 import * as path from 'path';
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 38d39e6..0bfcb9c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,10 +15,11 @@
     "skipDefaultLibCheck": true,
     "baseUrl": ".",
     "paths": {
-      "@griffel/e2e-typescript": ["e2e/typescript/src/index.ts"],
       "@griffel/babel-preset": ["packages/babel-preset/src/index.ts"],
       "@griffel/core": ["packages/core/src/index.ts"],
       "@griffel/devtools": ["packages/devtools/src/index.ts"],
+      "@griffel/e2e-typescript": ["e2e/typescript/src/index.ts"],
+      "@griffel/eslint-plugin": ["packages/eslint-plugin/src/index.ts"],
       "@griffel/jest-serializer": ["packages/jest-serializer/src/index.ts"],
       "@griffel/react": ["packages/react/src/index.ts"],
       "@griffel/webpack-loader": ["packages/webpack-loader/src/index.ts"]
diff --git a/workspace.json b/workspace.json
index 9ba29c7..0696a83 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1,13 +1,14 @@
 {
   "version": 2,
   "projects": {
-    "@griffel/e2e-typescript": "e2e/typescript",
     "@griffel/babel-preset": "packages/babel-preset",
     "@griffel/core": "packages/core",
+    "@griffel/devtools": "packages/devtools",
+    "@griffel/e2e-typescript": "e2e/typescript",
+    "@griffel/eslint-plugin": "packages/eslint-plugin",
     "@griffel/jest-serializer": "packages/jest-serializer",
     "@griffel/react": "packages/react",
     "@griffel/webpack-loader": "packages/webpack-loader",
-    "@griffel/website": "apps/website",
-    "@griffel/devtools": "packages/devtools"
+    "@griffel/website": "apps/website"
   }
 }
diff --git a/yarn.lock b/yarn.lock
index 16b0d76..3ad9a33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6623,6 +6623,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/scope-manager@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/scope-manager@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/visitor-keys": 5.20.0
+  checksum: 904fd43f559dc2579958496ffad837eca124940b4a172666f0ea54ed606074d9ec7d2bec0f2141c3f9a8b894dd2644817cb86809e79a7a73ecba2b7babcdb5c9
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/scope-manager@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/scope-manager@npm:5.3.1"
@@ -6633,6 +6643,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/types@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/types@npm:5.20.0"
+  checksum: d7f6e51e23f59feee8857340828c47a98a0dd5eaa1b045e936dc11199b55754cf78ae5cd8d56c1fafb1b5a40a6f472c1ac921072951217caffe3f06a717fa61c
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/types@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/types@npm:5.3.1"
@@ -6640,6 +6657,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/typescript-estree@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/typescript-estree@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/visitor-keys": 5.20.0
+    debug: ^4.3.2
+    globby: ^11.0.4
+    is-glob: ^4.0.3
+    semver: ^7.3.5
+    tsutils: ^3.21.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 2b709292b7df3675d1f8eaf2f4e1ecf491f70fc525012c6a0fb5164aa893c165317b0a419022b8b00aaed502864d5b5b84092b58a9950d2633248e8d7627abd8
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/typescript-estree@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/typescript-estree@npm:5.3.1"
@@ -6658,6 +6693,32 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/utils@npm:^5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/utils@npm:5.20.0"
+  dependencies:
+    "@types/json-schema": ^7.0.9
+    "@typescript-eslint/scope-manager": 5.20.0
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/typescript-estree": 5.20.0
+    eslint-scope: ^5.1.1
+    eslint-utils: ^3.0.0
+  peerDependencies:
+    eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+  checksum: e387cf96124e34d079804220c5cb9134148fb3efc68d852a344453e285e3016e0b7e37b11308ef58c0e7afc638f145002cebc27c5da0fd03e0c074ff97d8210e
+  languageName: node
+  linkType: hard
+
+"@typescript-eslint/visitor-keys@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/visitor-keys@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    eslint-visitor-keys: ^3.0.0
+  checksum: 1e1aa5f14fd60f1846ee26947d571953898dc82eb635a7eab3984c6b7db9bb8897743416713a129cc95c8cd63325cc0c64b3935d264f73100911fc5da76fc65f
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/visitor-keys@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/visitor-keys@npm:5.3.1"
@@ -13239,6 +13300,7 @@ __metadata:
     "@types/tmp": 0.2.3
     "@typescript-eslint/eslint-plugin": ~5.3.0
     "@typescript-eslint/parser": ~5.3.0
+    "@typescript-eslint/utils": ^5.20.0
     ajv: ^8.4.0
     babel-jest: 27.2.3
     babel-loader: 8.1.0


Test patch:
diff --git a/packages/core/src/runtime/compileCSS.test.ts b/packages/core/src/runtime/compileCSS.test.ts
index ea4bc06..386a986 100644
--- a/packages/core/src/runtime/compileCSS.test.ts
+++ b/packages/core/src/runtime/compileCSS.test.ts
@@ -1,12 +1,14 @@
 import { compileCSS, CompileCSSOptions, normalizePseudoSelector } from './compileCSS';
 
-const defaultOptions: Pick<CompileCSSOptions, 'rtlClassName' | 'className' | 'media' | 'pseudo' | 'support'> = {
-  className: 'foo',
-  rtlClassName: 'rtl-foo',
-  media: '',
-  pseudo: '',
-  support: '',
-};
+const defaultOptions: Pick<CompileCSSOptions, 'rtlClassName' | 'className' | 'media' | 'pseudo' | 'support' | 'layer'> =
+  {
+    className: 'foo',
+    rtlClassName: 'rtl-foo',
+    media: '',
+    pseudo: '',
+    support: '',
+    layer: '',
+  };
 
 describe('compileCSS', () => {
   it('handles pseudo', () => {
diff --git a/packages/core/src/runtime/getStyleBucketName.test.ts b/packages/core/src/runtime/getStyleBucketName.test.ts
index e18f439..93239d6 100644
--- a/packages/core/src/runtime/getStyleBucketName.test.ts
+++ b/packages/core/src/runtime/getStyleBucketName.test.ts
@@ -2,18 +2,19 @@ import { getStyleBucketName } from './getStyleBucketName';
 
 describe('getStyleBucketName', () => {
   it('returns bucketName based on mapping', () => {
-    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', '(max-width: 100px)', '')).toBe('t');
-    expect(getStyleBucketName(':active', '', '(display: table-cell)')).toBe('t');
+    expect(getStyleBucketName(':active', 'theme', '', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '', '(display: table-cell)')).toBe('t');
   });
 });
diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts
index 97938ba..3ceaa78 100644
--- a/packages/core/src/runtime/resolveStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveStyleRules.test.ts
@@ -345,7 +345,7 @@ describe('resolveStyleRules', () => {
       `);
     });
 
-    it('handles media queries with preudo selectors', () => {
+    it('handles media queries with pseudo selectors', () => {
       expect(
         resolveStyleRules({
           color: 'green',
@@ -393,6 +393,85 @@ describe('resolveStyleRules', () => {
       `);
     });
 
+    it('handles layer queries', () => {
+      expect(
+        resolveStyleRules({
+          color: 'green',
+          '@layer color': { color: 'red' },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fka9v86 {
+          color: green;
+        }
+        @layer color {
+          .f1hjcal7 {
+            color: red;
+          }
+        }
+      `);
+    });
+
+    it('handles layer queries with dots', () => {
+      expect(
+        resolveStyleRules({
+          '@layer framework.utilities': { color: 'red' },
+        }),
+      ).toMatchInlineSnapshot(`
+        @layer framework.utilities {
+          .faxdetk {
+            color: red;
+          }
+        }
+      `);
+    });
+    it('handles layer queries with pseudo selectors', () => {
+      expect(
+        resolveStyleRules({
+          color: 'green',
+          '@layer color': {
+            ':hover': {
+              color: 'red ',
+            },
+          },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fka9v86 {
+          color: green;
+        }
+        @layer color {
+          .f1jv0g3z:hover {
+            color: red;
+          }
+        }
+      `);
+    });
+
+    it('handles nested layer queries', () => {
+      expect(
+        resolveStyleRules({
+          color: 'red',
+          '@layer color': {
+            color: 'red',
+            '@layer theme': { color: 'red' },
+          },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fe3e8s9 {
+          color: red;
+        }
+        @layer color {
+          .f1hjcal7 {
+            color: red;
+          }
+        }
+        @layer color.theme {
+          .f132c7g3 {
+            color: red;
+          }
+        }
+      `);
+    });
+
     it('handles supports queries', () => {
       expect(
         resolveStyleRules({
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.test.ts b/packages/eslint-plugin/src/rules/no-shorthands.test.ts
new file mode 100644
index 0000000..36a39c3
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.test.ts
@@ -0,0 +1,64 @@
+import { TSESLint } from '@typescript-eslint/utils';
+import * as path from 'path';
+
+import { noShorthandsRule, RULE_NAME } from './no-shorthands';
+
+const ruleTester = new TSESLint.RuleTester({
+  parser: path.resolve('./node_modules/@typescript-eslint/parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run(RULE_NAME, noShorthandsRule, {
+  valid: [
+    {
+      name: 'without CSS shorthands',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: { backgroundColor: 'red' },
+});
+`,
+    },
+    {
+      name: 'CSS shorthands can be used as slot names',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  background: { backgroundColor: 'red' },
+});
+`,
+    },
+  ],
+
+  invalid: [
+    {
+      name: 'CSS shorthand or root level',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: { background: 'red' },
+});
+`,
+      errors: [{ messageId: 'shorthandFound' }],
+    },
+    {
+      name: 'CSS shorthand in a selector',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    ':hover': { background: 'red' }
+  },
+});
+`,
+      errors: [{ messageId: 'shorthandFound' }],
+    },
+  ],
+});