Repository: microsoft/griffel
Pull request #11 (2022-01-18 15:54:18)

feat: add React implementation
This PR moves existing code from `@fluentui/react-make-styles`.

Notable differences:
- `@fluentui/react-make-styles` used `useFluent()` from `@fluentui/react-shared-contexts` to get text direction, this was replaced with `TextDirectionProvider` & `useTextDirection()` hook
- `canUseDOM()` from `@fluentui/react-utilities` was inlined


Patch:
diff --git a/.github/workflows/ci-change.yml b/.github/workflows/ci-change.yml
new file mode 100644
index 0000000..9646f2f
--- /dev/null
+++ b/.github/workflows/ci-change.yml
@@ -0,0 +1,19 @@
+name: Changelog
+on:
+  pull_request:
+
+jobs:
+  pr:
+    runs-on: ubuntu-latest
+    if: ${{ github.event_name == 'pull_request' }}
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          ref: ${{ github.event.pull_request.head.ref }}
+          fetch-depth: 0
+
+      - uses: actions/setup-node@v1
+        with:
+          node-version: '14'
+
+      - run: npx beachball check --changehint "Run 'yarn change' to generate a change file"
diff --git a/.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c b/.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c
new file mode 100644
index 0000000..565cda1
--- /dev/null
+++ b/.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c
@@ -0,0 +1,13 @@
+diff --git a/src/utilities/buildable-libs-utils.js b/src/utilities/buildable-libs-utils.js
+index a3b897ae1a0f493f0d2a37598f988a6a78955f62..b22cddbcd84eca7d061b2396911b2f367307b951 100644
+--- a/src/utilities/buildable-libs-utils.js
++++ b/src/utilities/buildable-libs-utils.js
+@@ -60,7 +60,7 @@ function recursivelyCollectDependencies(project, projGraph, acc) {
+     (projGraph.dependencies[project] || []).forEach((dependency) => {
+         if (acc.indexOf(dependency.target) === -1) {
+             acc.push(dependency.target);
+-            recursivelyCollectDependencies(dependency.target, projGraph, acc);
++            // recursivelyCollectDependencies(dependency.target, projGraph, acc);
+         }
+     });
+     return acc;
diff --git a/bundle-size.config.js b/bundle-size.config.js
index 004583c..d7beaba 100644
--- a/bundle-size.config.js
+++ b/bundle-size.config.js
@@ -1,16 +1,14 @@
 const path = require('path');
 
 module.exports = {
-  webpack: (config) => {
+  webpack: config => {
     return {
       ...config,
       resolve: {
         ...config.resolve,
         alias: {
-          '@griffel/core': path.resolve(
-            __dirname,
-            './dist/packages/core/index.esm.js'
-          ),
+          '@griffel/core': path.resolve(__dirname, './dist/packages/core/index.esm.js'),
+          '@griffel/react': path.resolve(__dirname, './dist/packages/react/index.esm.js'),
         },
       },
     };
diff --git a/change/@griffel-react-b32b8d75-0ec2-47b3-b718-7a859cae50c9.json b/change/@griffel-react-b32b8d75-0ec2-47b3-b718-7a859cae50c9.json
new file mode 100644
index 0000000..8957635
--- /dev/null
+++ b/change/@griffel-react-b32b8d75-0ec2-47b3-b718-7a859cae50c9.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "initial release",
+  "packageName": "@griffel/react",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/package.json b/package.json
index 93be1dd..806ccfc 100644
--- a/package.json
+++ b/package.json
@@ -17,22 +17,22 @@
   },
   "devDependencies": {
     "@fluentui/bundle-size": "^1.1.4",
-    "@nrwl/cli": "13.4.4",
+    "@nrwl/cli": "13.4.5",
     "@nrwl/eslint-plugin-nx": "13.4.5",
     "@nrwl/jest": "13.4.5",
-    "@nrwl/js": "13.4.4",
+    "@nrwl/js": "13.4.5",
     "@nrwl/linter": "13.4.5",
     "@nrwl/react": "^13.4.5",
-    "@nrwl/tao": "13.4.4",
+    "@nrwl/tao": "13.4.5",
     "@nrwl/web": "13.4.5",
-    "@nrwl/workspace": "13.4.4",
+    "@nrwl/workspace": "13.4.5",
     "@testing-library/react": "12.1.2",
     "@testing-library/react-hooks": "7.0.2",
     "@types/jest": "27.0.2",
     "@types/node": "14.14.33",
     "@types/react": "17.0.30",
     "@types/react-dom": "17.0.9",
-    "@types/stylis": "^4.0.2",
+    "@types/stylis": "4.0.2",
     "@typescript-eslint/eslint-plugin": "~5.3.0",
     "@typescript-eslint/parser": "~5.3.0",
     "babel-jest": "27.2.3",
@@ -45,24 +45,25 @@
     "eslint-plugin-react": "7.27.0",
     "eslint-plugin-react-hooks": "4.2.0",
     "jest": "27.2.3",
-    "nano-staged": "^0.5.0",
-    "prettier": "^2.3.1",
+    "nano-staged": "0.5.0",
+    "prettier": "2.3.1",
+    "react": "17.0.2",
+    "react-dom": "17.0.2",
     "react-test-renderer": "17.0.2",
-    "simple-git-hooks": "^2.7.0",
+    "simple-git-hooks": "2.7.0",
     "ts-jest": "27.0.5",
     "typescript": "~4.4.3"
   },
   "dependencies": {
     "@emotion/hash": "^0.8.0",
     "csstype": "^2.6.19",
-    "react": "17.0.2",
-    "react-dom": "17.0.2",
     "rtl-css-js": "^1.15.0",
     "stylis": "^4.0.13",
     "tslib": "^2.1.0"
   },
   "resolutions": {
     "@nrwl/web": "patch:@nrwl/web@npm:13.4.5#.yarn/patches/@nrwl-web-npm-13.4.5-d7e9ea40d2",
+    "@nrwl/workspace": "patch:@nrwl/workspace@npm:13.4.5#.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c",
     "beachball": "patch:beachball@npm:2.21.0#.yarn/patches/beachball-npm-2.21.0-85c8ba3d6b"
   }
 }
diff --git a/packages/react/.babelrc b/packages/react/.babelrc
new file mode 100644
index 0000000..8945e1b
--- /dev/null
+++ b/packages/react/.babelrc
@@ -0,0 +1,9 @@
+{
+  "plugins": ["annotate-pure-calls"],
+  "presets": ["@babel/preset-react"],
+  "env": {
+    "test": {
+      "presets": ["@nrwl/react/babel"]
+    }
+  }
+}
diff --git a/packages/react/.eslintrc.json b/packages/react/.eslintrc.json
new file mode 100644
index 0000000..734ddac
--- /dev/null
+++ b/packages/react/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/packages/react/README.md b/packages/react/README.md
new file mode 100644
index 0000000..757c84f
--- /dev/null
+++ b/packages/react/README.md
@@ -0,0 +1,204 @@
+# Griffel for React
+
+## Install
+
+```bash
+npm install @griffel/react
+# or
+yarn add @griffel/react
+```
+
+## API
+
+### `makeStyles()`
+
+Is used to define styles, returns a React hook that should be called inside a component:
+
+```tsx
+import { makeStyles } from '@griffel/react';
+
+const useClasses = makeStyles({
+  button: { color: 'red' },
+  icon: { paddingLeft: '5px' },
+});
+
+function Component() {
+  const classes = useClasses();
+
+  return (
+    <div>
+      <button className={classes.button} />
+      <span className={classes.icon} />
+    </div>
+  );
+}
+```
+
+### `mergeClasses()`
+
+> 💡 **It is not possible to simply concatenate classes returned by `useClasses()`.**
+
+There are cases where you need to merge classes from multiple `useClasses` calls.
+
+To properly merge the classes, you need to use `mergeClasses()` function, which performs merge and deduplication of atomic classes generated by `makeStyles()`.
+
+```tsx
+import { mergeClasses, makeStyles } from '@griffel/react';
+
+const useClasses = makeStyles({
+  blueBold: {
+    color: 'blue',
+    fontWeight: 'bold',
+  },
+  red: {
+    color: 'red',
+  },
+});
+
+function Component() {
+  const classes = useClasses();
+
+  const firstClassName = mergeClasses(classes.blueBold, classes.red); // { color: 'red', fontWeight: 'bold' }
+  const secondClassName = mergeClasses(classes.red, classes.blueBold); // { color: 'blue', fontWeight: 'bold' }
+
+  return (
+    <>
+      <div className={firstClassName} />
+      <div className={secondClassName} />
+    </>
+  );
+}
+```
+
+## `makeStaticStyles()`
+
+Creates styles attached to a global selector. Styles can be defined via objects:
+
+```tsx
+import { makeStaticStyles } from '@griffel/react';
+
+const useStaticStyles = makeStaticStyles({
+  '@font-face': {
+    fontFamily: 'Open Sans',
+    src: `url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
+         url("/fonts/OpenSans-Regular-webfont.woff") format("woff")`,
+  },
+  body: {
+    background: 'red',
+  },
+
+  /**
+   * ⚠️ nested and pseudo selectors are not supported for this scenario via nesting
+   *
+   * Not supported:
+   * .some {
+   *   .class { ... },
+   *   ':hover': { ... }
+   * }
+   *
+   * Supported:
+   * '.some.class': { ... }
+   * '.some.class:hover': { ... }
+   */
+});
+
+function App() {
+  useStaticStyles();
+
+  return <div />;
+}
+```
+
+Or with string & arrays of strings/objects:
+
+```tsx
+import { makeStaticStyles } from '@griffel/react';
+
+const useStaticStyles1 = makeStaticStyles('body { background: red; } .foo { color: green; }');
+const useStaticStyles2 = makeStaticStyles([
+  {
+    '@font-face': {
+      fontFamily: 'My Font',
+      src: `url(my_font.woff)`,
+    },
+  },
+  'html { line-height: 20px; }',
+]);
+
+function App() {
+  useStaticStyles1();
+  useStaticStyles2();
+
+  return <div />;
+}
+```
+
+## Features support
+
+### 📃 pseudo & class selectors, at-rules, global styles
+
+`makeStyles()` supports pseudo, class selectors and at-rules.
+
+```ts
+import { makeStyles } from '@griffel/react';
+
+const useClasses = makeStyles({
+  root: {
+    ':active': { color: 'pink' },
+    ':hover': { color: 'blue' },
+    // :link, :focus, etc.
+
+    '.foo': { color: 'black' },
+    ':nth-child(2n)': { background: '#fafafa' },
+
+    '@media screen and (max-width: 992px)': { color: 'orange' },
+    '@supports (display: grid)': { color: 'red' },
+  },
+});
+```
+
+Another useful feature is `:global()` selector, it allows connecting local styles with global selectors.
+
+```ts
+import { makeStyles } from '@griffel/react';
+
+const useClasses = makeStyles({
+  root: {
+    ':global(html[data-whatintent="mouse"])': { background: 'yellow' },
+    // outputs: html[data-whatintent="mouse"] .abcd { background: yellow }
+  },
+});
+```
+
+### 🎞 `keyframes` (animations)
+
+`keyframes` are supported via `animationName` property that can be defined as an object or an array of objects:
+
+```tsx
+import { makeStyles } from '@griffel/react';
+
+const useClasses = makeStyles({
+  root: {
+    animationIterationCount: 'infinite',
+    animationDuration: '3s',
+    animationName: {
+      from: { transform: 'rotate(0deg)' },
+      to: { transform: 'rotate(360deg)' },
+    },
+  },
+  array: {
+    animationIterationCount: 'infinite',
+    animationDuration: '3s',
+    animationName: [
+      {
+        from: { transform: 'rotate(0deg)' },
+        to: { transform: 'rotate(360deg)' },
+      },
+      {
+        from: { height: '100px' },
+        to: { height: '200px' },
+      },
+    ],
+  },
+});
+```
diff --git a/packages/react/bundle-size/makeStaticStyles-runtime.fixture.js b/packages/react/bundle-size/makeStaticStyles-runtime.fixture.js
new file mode 100644
index 0000000..78a8bd4
--- /dev/null
+++ b/packages/react/bundle-size/makeStaticStyles-runtime.fixture.js
@@ -0,0 +1,7 @@
+import { makeStaticStyles } from '@griffel/react';
+
+console.log(makeStaticStyles);
+
+export default {
+  name: 'makeStaticStyles (runtime)',
+};
diff --git a/packages/react/bundle-size/makeStyles-runtime.fixture.js b/packages/react/bundle-size/makeStyles-runtime.fixture.js
new file mode 100644
index 0000000..88b448d
--- /dev/null
+++ b/packages/react/bundle-size/makeStyles-runtime.fixture.js
@@ -0,0 +1,7 @@
+import { makeStyles, mergeClasses } from '@griffel/react';
+
+console.log(makeStyles, mergeClasses);
+
+export default {
+  name: 'makeStyles + mergeClasses (runtime)',
+};
diff --git a/packages/react/bundle-size/makeStyles.fixture.js b/packages/react/bundle-size/makeStyles.fixture.js
new file mode 100644
index 0000000..92d5b23
--- /dev/null
+++ b/packages/react/bundle-size/makeStyles.fixture.js
@@ -0,0 +1,7 @@
+import { __styles, mergeClasses } from '@griffel/react';
+
+console.log(__styles, mergeClasses);
+
+export default {
+  name: 'makeStyles + mergeClasses (build time)',
+};
diff --git a/packages/react/jest.config.js b/packages/react/jest.config.js
new file mode 100644
index 0000000..91675a2
--- /dev/null
+++ b/packages/react/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  displayName: 'react',
+  preset: '../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]sx?$': 'babel-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../coverage/packages/react',
+};
diff --git a/packages/react/package.json b/packages/react/package.json
new file mode 100644
index 0000000..13cb26d
--- /dev/null
+++ b/packages/react/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "@griffel/react",
+  "version": "0.0.1",
+  "description": "React implementation of Atomic CSS-in-JS",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/microsoft/griffel"
+  },
+  "sideEffects": false,
+  "peerDependencies": {
+    "react": ">=16.8.0 <18.0.0"
+  }
+}
diff --git a/packages/react/project.json b/packages/react/project.json
new file mode 100644
index 0000000..d84bc63
--- /dev/null
+++ b/packages/react/project.json
@@ -0,0 +1,57 @@
+{
+  "root": "packages/react",
+  "sourceRoot": "packages/react/src",
+  "projectType": "library",
+  "tags": [],
+  "targets": {
+    "build": {
+      "executor": "@nrwl/web:rollup",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "buildableProjectDepsInPackageJsonType": "dependencies",
+        "outputPath": "dist/packages/react",
+        "tsConfig": "packages/react/tsconfig.lib.json",
+        "project": "packages/react/package.json",
+        "entryFile": "packages/react/src/index.ts",
+        "compiler": "babel",
+        "rollupConfig": ["@nrwl/react/plugins/bundle-rollup", "tools/getRollupOptions.js"],
+        "format": ["esm", "cjs"],
+        "assets": [
+          {
+            "glob": "packages/react/README.md",
+            "input": ".",
+            "output": "."
+          },
+          {
+            "glob": "LICENSE.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["packages/react/**/*.{ts,tsx,js,jsx}"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/packages/react"],
+      "options": {
+        "jestConfig": "packages/react/jest.config.js",
+        "passWithNoTests": true
+      }
+    },
+    "bundle-size": {
+      "executor": "@nrwl/workspace:run-commands",
+      "dependsOn": [{ "target": "build", "projects": "self" }],
+      "options": {
+        "cwd": "packages/react",
+        "commands": [{ "command": "bundle-size measure" }]
+      }
+    }
+  }
+}
diff --git a/packages/react/src/RendererContext.tsx b/packages/react/src/RendererContext.tsx
new file mode 100644
index 0000000..e9ebc9e
--- /dev/null
+++ b/packages/react/src/RendererContext.tsx
@@ -0,0 +1,59 @@
+import { createDOMRenderer, rehydrateRendererCache } from '@griffel/core';
+import * as React from 'react';
+import type { GriffelRenderer } from '@griffel/core';
+
+export interface RendererProviderProps {
+  /** An instance of Griffel renderer. */
+  renderer: GriffelRenderer;
+
+  /**
+   * Document used to insert CSS variables to head
+   */
+  targetDocument?: Document;
+}
+
+/**
+ * Verifies if an application can use DOM.
+ */
+function canUseDOM(): boolean {
+  return (
+    typeof window !== 'undefined' &&
+    !!(
+      window.document &&
+      // eslint-disable-next-line deprecation/deprecation
+      window.document.createElement
+    )
+  );
+}
+
+/**
+ * @private
+ */
+const RendererContext = React.createContext<GriffelRenderer>(createDOMRenderer());
+
+/**
+ * @public
+ */
+export const RendererProvider: React.FC<RendererProviderProps> = ({ children, renderer, targetDocument }) => {
+  if (canUseDOM()) {
+    // This if statement technically breaks the rules of hooks, but is safe because the condition never changes after
+    // mounting.
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    React.useMemo(() => {
+      // "rehydrateCache()" can't be called in effects as it needs to be called before any component will be rendered to
+      // avoid double insertion of classes
+      rehydrateRendererCache(renderer, targetDocument);
+    }, [renderer, targetDocument]);
+  }
+
+  return <RendererContext.Provider value={renderer}>{children}</RendererContext.Provider>;
+};
+
+/**
+ * Returns an instance of current makeStyles() renderer.
+ *
+ * @private
+ */
+export function useRenderer(): GriffelRenderer {
+  return React.useContext(RendererContext);
+}
diff --git a/packages/react/src/TextDirectionContext.tsx b/packages/react/src/TextDirectionContext.tsx
new file mode 100644
index 0000000..3d4e919
--- /dev/null
+++ b/packages/react/src/TextDirectionContext.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+
+export interface TextDirectionProviderProps {
+  /** Indicates the directionality of the element's text. */
+  dir: 'ltr' | 'rtl';
+}
+
+/**
+ * @private
+ */
+const TextDirectionContext = React.createContext<'ltr' | 'rtl'>('ltr');
+
+/**
+ * @public
+ */
+export const TextDirectionProvider: React.FC<TextDirectionProviderProps> = ({ children, dir }) => {
+  return <TextDirectionContext.Provider value={dir}>{children}</TextDirectionContext.Provider>;
+};
+
+/**
+ * Returns current directionality of the element's text.
+ *
+ * @private
+ */
+export function useTextDirection() {
+  return React.useContext(TextDirectionContext);
+}
diff --git a/packages/react/src/__styles.ts b/packages/react/src/__styles.ts
new file mode 100644
index 0000000..345627e
--- /dev/null
+++ b/packages/react/src/__styles.ts
@@ -0,0 +1,25 @@
+import { __styles as vanillaStyles } from '@griffel/core';
+import type { CSSClassesMapBySlot, CSSRulesByBucket } from '@griffel/core';
+
+import { useRenderer } from './RendererContext';
+import { useTextDirection } from './TextDirectionContext';
+
+/**
+ * A version of makeStyles() that accepts build output as an input and skips all runtime transforms.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function __styles<Slots extends string>(
+  classesMapBySlot: CSSClassesMapBySlot<Slots>,
+  cssRules: CSSRulesByBucket,
+) {
+  const getStyles = vanillaStyles(classesMapBySlot, cssRules);
+
+  return function useClasses(): Record<Slots, string> {
+    const dir = useTextDirection();
+    const renderer = useRenderer();
+
+    return getStyles({ dir, renderer });
+  };
+}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
new file mode 100644
index 0000000..ee3ced6
--- /dev/null
+++ b/packages/react/src/index.ts
@@ -0,0 +1,12 @@
+export { shorthands, mergeClasses, createDOMRenderer } from '@griffel/core';
+export type { GriffelStyle } from '@griffel/core';
+
+export { makeStyles } from './makeStyles';
+export { makeStaticStyles } from './makeStaticStyles';
+export { renderToStyleElements } from './renderToStyleElements';
+
+export { RendererProvider } from './RendererContext';
+export { TextDirectionProvider } from './TextDirectionContext';
+
+// Private exports, are used by build time transforms
+export { __styles } from './__styles';
diff --git a/packages/react/src/makeStaticStyles.ts b/packages/react/src/makeStaticStyles.ts
new file mode 100644
index 0000000..a9a1c2a
--- /dev/null
+++ b/packages/react/src/makeStaticStyles.ts
@@ -0,0 +1,20 @@
+import { makeStaticStyles as vanillaMakeStaticStyles } from '@griffel/core';
+
+import { useRenderer } from './RendererContext';
+import type { GriffelStaticStyles, MakeStaticStylesOptions } from '@griffel/core';
+
+export function makeStaticStyles<Selectors>(styles: GriffelStaticStyles | GriffelStaticStyles[]) {
+  const getStyles = vanillaMakeStaticStyles(styles);
+
+  if (process.env.NODE_ENV === 'test') {
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    return () => {};
+  }
+
+  return function useStaticStyles(): void {
+    const renderer = useRenderer();
+    const options: MakeStaticStylesOptions = { renderer };
+
+    return getStyles(options);
+  };
+}
diff --git a/packages/react/src/makeStyles.ts b/packages/react/src/makeStyles.ts
new file mode 100644
index 0000000..ecd4b7c
--- /dev/null
+++ b/packages/react/src/makeStyles.ts
@@ -0,0 +1,38 @@
+import { makeStyles as vanillaMakeStyles } from '@griffel/core';
+import * as React from 'react';
+import type { GriffelStyle } from '@griffel/core';
+
+import { useRenderer } from './RendererContext';
+import { useTextDirection } from './TextDirectionContext';
+
+function isInsideComponent() {
+  try {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    React.useContext({} as unknown as React.Context<unknown>);
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+export function makeStyles<Slots extends string | number>(stylesBySlots: Record<Slots, GriffelStyle>) {
+  const getStyles = vanillaMakeStyles(stylesBySlots);
+
+  if (process.env.NODE_ENV !== 'production') {
+    if (isInsideComponent()) {
+      throw new Error(
+        [
+          "makeStyles(): this function cannot be called in component's scope.",
+          'All makeStyles() calls should be top level i.e. in a root scope of a file.',
+        ].join(' '),
+      );
+    }
+  }
+
+  return function useClasses(): Record<Slots, string> {
+    const dir = useTextDirection();
+    const renderer = useRenderer();
+
+    return getStyles({ dir, renderer });
+  };
+}
diff --git a/packages/react/src/renderToStyleElements.ts b/packages/react/src/renderToStyleElements.ts
new file mode 100644
index 0000000..946aab4
--- /dev/null
+++ b/packages/react/src/renderToStyleElements.ts
@@ -0,0 +1,48 @@
+import { styleBucketOrdering } from '@griffel/core';
+import * as React from 'react';
+import type { GriffelRenderer, StyleBucketName } from '@griffel/core';
+
+type CSSRulesGroupedByStyleBucket = Record<StyleBucketName, string[]>;
+
+/**
+ * This method returns a list of <style> React elements with the rendered CSS. This is useful for Server-Side rendering.
+ *
+ * @public
+ */
+export function renderToStyleElements(renderer: GriffelRenderer): React.ReactElement[] {
+  const styles = styleBucketOrdering.reduce<CSSRulesGroupedByStyleBucket>((acc, bucketName) => {
+    return { ...acc, [bucketName]: [] };
+  }, {} as CSSRulesGroupedByStyleBucket);
+
+  // eslint-disable-next-line guard-for-in
+  for (const cssRule in renderer.insertionCache) {
+    const bucketName: StyleBucketName = renderer.insertionCache[cssRule];
+
+    styles[bucketName].push(cssRule);
+  }
+
+  return (Object.keys(styles) as StyleBucketName[])
+    .map(bucketName => {
+      const cssRules = styles[bucketName].join('');
+
+      // We don't want to create empty style elements
+      if (cssRules.length === 0) {
+        return null;
+      }
+
+      return React.createElement('style', {
+        key: bucketName,
+
+        // TODO: support "nonce"
+        // ...renderer.styleNodeAttributes,
+
+        'data-make-styles-bucket': bucketName || 'default',
+        'data-make-styles-rehydration': true,
+
+        dangerouslySetInnerHTML: {
+          __html: cssRules,
+        },
+      });
+    })
+    .filter(Boolean) as React.ReactElement[];
+}
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
new file mode 100644
index 0000000..a6914fd
--- /dev/null
+++ b/packages/react/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "react",
+    "allowJs": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": 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/react/tsconfig.lib.json b/packages/react/tsconfig.lib.json
new file mode 100644
index 0000000..4b54c42
--- /dev/null
+++ b/packages/react/tsconfig.lib.json
@@ -0,0 +1,18 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "types": ["node", "environment"]
+  },
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+    "**/*.spec.tsx",
+    "**/*.test.tsx",
+    "**/*.spec.js",
+    "**/*.test.js",
+    "**/*.spec.jsx",
+    "**/*.test.jsx"
+  ],
+  "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/packages/react/tsconfig.spec.json b/packages/react/tsconfig.spec.json
new file mode 100644
index 0000000..25c45ce
--- /dev/null
+++ b/packages/react/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/tools/getRollupOptions.js b/tools/getRollupOptions.js
index c5ea220..d1c9bbe 100644
--- a/tools/getRollupOptions.js
+++ b/tools/getRollupOptions.js
@@ -1,14 +1,20 @@
 function getRollupOptions(/** @type {import('rollup').RollupOptions} */ options) {
+  // Following options are overridden:
+  // - `preserveModules` & `preserveModulesRoot` to stop bundling to a single file and prevent bundle size issues
+  // - `interop` to not include interop for `import * as` syntax
+
   if (Array.isArray(options.output)) {
     options.output.forEach(o => {
       o.preserveModules = true;
       o.preserveModulesRoot = 'src';
+      o.interop = false;
     });
   } else {
     options.output = {
       ...options.output,
       preserveModules: true,
       preserveModulesRoot: 'src',
+      interop: false,
     };
   }
 
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 0925d89..a24996e 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,7 +15,8 @@
     "skipDefaultLibCheck": true,
     "baseUrl": ".",
     "paths": {
-      "@griffel/core": ["packages/core/src/index.ts"]
+      "@griffel/core": ["packages/core/src/index.ts"],
+      "@griffel/react": ["packages/react/src/index.ts"]
     },
     "typeRoots": ["node_modules/@types", "./typings"]
   },
diff --git a/workspace.json b/workspace.json
index 8b9593d..df19385 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1,6 +1,7 @@
 {
   "version": 2,
   "projects": {
-    "core": "packages/core"
+    "core": "packages/core",
+    "react": "packages/react"
   }
 }
diff --git a/yarn.lock b/yarn.lock
index 7981612..1c2de56 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1957,22 +1957,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/cli@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/cli@npm:13.4.4"
-  dependencies:
-    "@nrwl/tao": 13.4.4
-    chalk: 4.1.0
-    enquirer: ~2.3.6
-    v8-compile-cache: 2.3.0
-    yargs: 15.4.1
-    yargs-parser: 20.0.0
-  bin:
-    nx: bin/nx.js
-  checksum: 3de18c3502effd27c6925fc7633a7762f06531678b3c0f1f96a3ff57dbd1e67bb9fced7aca34d5f84484809da592832bcfca36cafae69d6a1e9f092cd28ca571
-  languageName: node
-  linkType: hard
-
 "@nrwl/cli@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/cli@npm:13.4.5"
@@ -2016,20 +2000,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/devkit@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/devkit@npm:13.4.4"
-  dependencies:
-    "@nrwl/tao": 13.4.4
-    ejs: ^3.1.5
-    ignore: ^5.0.4
-    rxjs: ^6.5.4
-    semver: 7.3.4
-    tslib: ^2.3.0
-  checksum: 70297576e99e990c87a10f0e60c3951932e7ca6e3cd994b1b6df15d2ccfc4b783ccc982ff7838c4f673d5a1e97305d52a622df91c84ea9c123a7c70032a2cf11
-  languageName: node
-  linkType: hard
-
 "@nrwl/devkit@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/devkit@npm:13.4.5"
@@ -2061,25 +2031,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/jest@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/jest@npm:13.4.4"
-  dependencies:
-    "@jest/reporters": 27.2.2
-    "@jest/test-result": 27.2.2
-    "@nrwl/devkit": 13.4.4
-    chalk: 4.1.0
-    identity-obj-proxy: 3.0.0
-    jest-config: 27.2.2
-    jest-resolve: 27.2.2
-    jest-util: 27.2.0
-    resolve.exports: 1.1.0
-    rxjs: ^6.5.4
-    tslib: ^2.3.0
-  checksum: 6f705243ef7cfdceb6ecd0982560d46f6cfd8c9b7ee0e769ac06f9f6f172b577265056418959931aa23d7a7d443d7aa2dd6ed4613ed36efb1ff9a582cccbe5ac
-  languageName: node
-  linkType: hard
-
 "@nrwl/jest@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/jest@npm:13.4.5"
@@ -2099,24 +2050,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/js@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/js@npm:13.4.4"
-  dependencies:
-    "@nrwl/devkit": 13.4.4
-    "@nrwl/jest": 13.4.4
-    "@nrwl/linter": 13.4.4
-    "@nrwl/workspace": 13.4.4
-    chalk: 4.1.0
-    js-tokens: ^4.0.0
-    rxjs: ^6.5.4
-    rxjs-for-await: 0.0.2
-    source-map-support: 0.5.19
-    tree-kill: 1.2.2
-  checksum: 26ec2c59a9a58bc3882e391752ef25fcae78446ff5c5f4a560735495150599408903353d51da6ce4e3d88960a27a5cd47edbae696f124d38c7544c7a62122c24
-  languageName: node
-  linkType: hard
-
 "@nrwl/js@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/js@npm:13.4.5"
@@ -2135,22 +2068,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/linter@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/linter@npm:13.4.4"
-  dependencies:
-    "@nrwl/devkit": 13.4.4
-    "@nrwl/jest": 13.4.4
-    "@phenomnomnominal/tsquery": 4.1.1
-    eslint: 8.2.0
-    glob: 7.1.4
-    minimatch: 3.0.4
-    tmp: ~0.2.1
-    tslib: ^2.3.0
-  checksum: 1094e26af710f1874a640a2980c5cfcaf80b5842fca517afcde4f9b78a9416aad35ca10987406e9a40f5bc3d140ee50326985c3525e7c27c234e100b2cc5768b
-  languageName: node
-  linkType: hard
-
 "@nrwl/linter@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/linter@npm:13.4.5"
@@ -2214,29 +2131,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/tao@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/tao@npm:13.4.4"
-  dependencies:
-    chalk: 4.1.0
-    enquirer: ~2.3.6
-    fast-glob: 3.2.7
-    fs-extra: ^9.1.0
-    ignore: ^5.0.4
-    jsonc-parser: 3.0.0
-    nx: 13.4.4
-    rxjs: ^6.5.4
-    rxjs-for-await: 0.0.2
-    semver: 7.3.4
-    tmp: ~0.2.1
-    tslib: ^2.3.0
-    yargs-parser: 20.0.0
-  bin:
-    tao: index.js
-  checksum: de21b5350cbbb6a525db65789b9281989569fb10c8bc923834801e988a3c6a4eead3cf1c15ecb8cd343736bcb343c8ff5f3719e6254e6063c83864b384362644
-  languageName: node
-  linkType: hard
-
 "@nrwl/tao@npm:13.4.5":
   version: 13.4.5
   resolution: "@nrwl/tao@npm:13.4.5"
@@ -2444,14 +2338,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nrwl/workspace@npm:13.4.4":
-  version: 13.4.4
-  resolution: "@nrwl/workspace@npm:13.4.4"
+"@nrwl/workspace@npm:13.4.5":
+  version: 13.4.5
+  resolution: "@nrwl/workspace@npm:13.4.5"
   dependencies:
-    "@nrwl/cli": 13.4.4
-    "@nrwl/devkit": 13.4.4
-    "@nrwl/jest": 13.4.4
-    "@nrwl/linter": 13.4.4
+    "@nrwl/cli": 13.4.5
+    "@nrwl/devkit": 13.4.5
+    "@nrwl/jest": 13.4.5
+    "@nrwl/linter": 13.4.5
     "@parcel/watcher": 2.0.4
     chalk: 4.1.0
     chokidar: ^3.5.1
@@ -2479,13 +2373,13 @@ __metadata:
   peerDependenciesMeta:
     prettier:
       optional: true
-  checksum: f4ff9fa8e33c05540e535540b32d6204021b1eff53f7d60d4055ab9408fc9d507fd8802a061e84cbab68912376ccd776dc4b0e8ae69ebb9acb387354d630b79b
+  checksum: 46912c862587d301c68ff6fcf6beebd2f0ac2bd63c2f518e7a84d9bc149c2375511c089b8fa0991ed2daae0526a34d0e94e6e5edd54eff0c08d59433785e4bb8
   languageName: node
   linkType: hard
 
-"@nrwl/workspace@npm:13.4.5":
+"@nrwl/workspace@patch:@nrwl/workspace@npm:13.4.5#.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c::locator=griffel-repository%40workspace%3A.":
   version: 13.4.5
-  resolution: "@nrwl/workspace@npm:13.4.5"
+  resolution: "@nrwl/workspace@patch:@nrwl/workspace@npm%3A13.4.5#.yarn/patches/@nrwl-workspace-npm-13.4.5-233a3ec85c::version=13.4.5&hash=4872e0&locator=griffel-repository%40workspace%3A."
   dependencies:
     "@nrwl/cli": 13.4.5
     "@nrwl/devkit": 13.4.5
@@ -2518,7 +2412,7 @@ __metadata:
   peerDependenciesMeta:
     prettier:
       optional: true
-  checksum: 46912c862587d301c68ff6fcf6beebd2f0ac2bd63c2f518e7a84d9bc149c2375511c089b8fa0991ed2daae0526a34d0e94e6e5edd54eff0c08d59433785e4bb8
+  checksum: 7c22cbfc206d51f0b2b41a835540211ca5ce2d8555a623a833397b9d81bdffa56bae6a0ae521d4f5a92413d068c19b73c3ae454d924618eab64c856ddfbe1265
   languageName: node
   linkType: hard
 
@@ -3370,7 +3264,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/stylis@npm:^4.0.2":
+"@types/stylis@npm:4.0.2":
   version: 4.0.2
   resolution: "@types/stylis@npm:4.0.2"
   checksum: 5461290afd6e7fbbd713f4896b4c586fa6f3072ef8addcb062feaf112a50168477df98ab474d9b603b19b879696aab6932c5f0bb3cda7472ab9c1622ee7fea60
@@ -7309,22 +7203,22 @@ __metadata:
   dependencies:
     "@emotion/hash": ^0.8.0
     "@fluentui/bundle-size": ^1.1.4
-    "@nrwl/cli": 13.4.4
+    "@nrwl/cli": 13.4.5
     "@nrwl/eslint-plugin-nx": 13.4.5
     "@nrwl/jest": 13.4.5
-    "@nrwl/js": 13.4.4
+    "@nrwl/js": 13.4.5
     "@nrwl/linter": 13.4.5
     "@nrwl/react": ^13.4.5
-    "@nrwl/tao": 13.4.4
+    "@nrwl/tao": 13.4.5
     "@nrwl/web": 13.4.5
-    "@nrwl/workspace": 13.4.4
+    "@nrwl/workspace": 13.4.5
     "@testing-library/react": 12.1.2
     "@testing-library/react-hooks": 7.0.2
     "@types/jest": 27.0.2
     "@types/node": 14.14.33
     "@types/react": 17.0.30
     "@types/react-dom": 17.0.9
-    "@types/stylis": ^4.0.2
+    "@types/stylis": 4.0.2
     "@typescript-eslint/eslint-plugin": ~5.3.0
     "@typescript-eslint/parser": ~5.3.0
     babel-jest: 27.2.3
@@ -7338,13 +7232,13 @@ __metadata:
     eslint-plugin-react: 7.27.0
     eslint-plugin-react-hooks: 4.2.0
     jest: 27.2.3
-    nano-staged: ^0.5.0
-    prettier: ^2.3.1
+    nano-staged: 0.5.0
+    prettier: 2.3.1
     react: 17.0.2
     react-dom: 17.0.2
     react-test-renderer: 17.0.2
     rtl-css-js: ^1.15.0
-    simple-git-hooks: ^2.7.0
+    simple-git-hooks: 2.7.0
     stylis: ^4.0.13
     ts-jest: 27.0.5
     tslib: ^2.1.0
@@ -9824,7 +9718,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nano-staged@npm:^0.5.0":
+"nano-staged@npm:0.5.0":
   version: 0.5.0
   resolution: "nano-staged@npm:0.5.0"
   dependencies:
@@ -10054,17 +9948,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nx@npm:13.4.4":
-  version: 13.4.4
-  resolution: "nx@npm:13.4.4"
-  dependencies:
-    "@nrwl/cli": 13.4.4
-  bin:
-    nx: bin/nx.js
-  checksum: 57f463551e2229b308e6f5ee7ad978a6c73b200f25252b6e97dae5db1dd628c688a63f5c72e930fccbbff157656b8844dcba432f1de4afc35f6d5cd34a5e646b
-  languageName: node
-  linkType: hard
-
 "nx@npm:13.4.5":
   version: 13.4.5
   resolution: "nx@npm:13.4.5"
@@ -11123,7 +11006,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"prettier@npm:^2.2.1, prettier@npm:^2.3.1":
+"prettier@npm:2.3.1":
+  version: 2.3.1
+  resolution: "prettier@npm:2.3.1"
+  bin:
+    prettier: bin-prettier.js
+  checksum: 3b37731ff7150feecf19736c77c790e7e276b404ac9af81cbaf87cfecefc48ef9a864f34c2a5caf5955378b8f2525984b8611703a0d9c1f052b4cfa6eb35899f
+  languageName: node
+  linkType: hard
+
+"prettier@npm:^2.2.1":
   version: 2.5.1
   resolution: "prettier@npm:2.5.1"
   bin:
@@ -12228,7 +12120,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"simple-git-hooks@npm:^2.7.0":
+"simple-git-hooks@npm:2.7.0":
   version: 2.7.0
   resolution: "simple-git-hooks@npm:2.7.0"
   bin:


Test patch:
diff --git a/packages/react/src/createDOMRenderer.test.tsx b/packages/react/src/createDOMRenderer.test.tsx
new file mode 100644
index 0000000..8a23987
--- /dev/null
+++ b/packages/react/src/createDOMRenderer.test.tsx
@@ -0,0 +1,93 @@
+import { createDOMRenderer } from '@griffel/core';
+import * as React from 'react';
+import { hydrate } from 'react-dom';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import { makeStyles } from './makeStyles';
+import { RendererProvider } from './RendererContext';
+import { renderToStyleElements } from './renderToStyleElements';
+
+describe('createDOMRenderer', () => {
+  it('rehydrateCache() avoids double insertion', () => {
+    // This test validates a scenario for Server-Side rendering
+
+    const clientRenderer = createDOMRenderer(document);
+    const serverRenderer = createDOMRenderer(
+      // we should use "null" as "undefined" will fallback to "document" which is present in this environment
+      null as unknown as undefined,
+    );
+
+    const useExampleStyles = makeStyles({
+      root: {
+        animationName: {
+          from: { height: '10px' },
+          to: { height: '20px' },
+        },
+
+        color: 'red',
+        '@media screen and (max-width: 992px)': { ':hover': { color: 'blue' } },
+      },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.root} />;
+    };
+
+    //
+    // Server
+    // A "server" renders components to static HTML that will be transferred to a client
+    //
+
+    const componentHTML = renderToStaticMarkup(
+      <RendererProvider renderer={serverRenderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+    const stylesHTML = renderToStaticMarkup(<>{renderToStyleElements(serverRenderer)}</>);
+
+    // There is no DOM on a server, style nodes should not be present
+    expect(document.querySelector('style')).toBe(null);
+
+    //
+    // Client
+    // Creates an element to render components and inserts HTML rendered from a server
+    //
+
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+
+    container.innerHTML = componentHTML;
+    document.head.innerHTML = stylesHTML;
+
+    // As all style came from a server, we should not insert any CSS on a client
+    // (this tests internal implementation, but there is no other way?)
+    const styleElementsBeforeHydration = document.querySelectorAll<HTMLStyleElement>('style');
+    const insertRules = [...(styleElementsBeforeHydration as unknown as HTMLStyleElement[])].map(styleEl =>
+      jest.spyOn(styleEl.sheet!, 'insertRule'),
+    );
+
+    hydrate(
+      // "RendererProvider" is not required there, we need it only for Jest spies
+      <RendererProvider renderer={clientRenderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+      container,
+    );
+
+    const styleElementsAfterHydration = document.querySelectorAll<HTMLStyleElement>('style');
+
+    // We also would to ensure that new elements have not been inserted
+    expect(styleElementsBeforeHydration.length).toBe(styleElementsAfterHydration.length);
+
+    // Following rules are present in cache:
+    // - "animationName"
+    // - "color"
+    // - @keyframes + prefixed
+    // - @media
+    expect(Object.keys(clientRenderer.insertionCache)).toHaveLength(5);
+    insertRules.forEach(insertRule => {
+      expect(insertRule).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/packages/react/src/makeStyles.test.tsx b/packages/react/src/makeStyles.test.tsx
new file mode 100644
index 0000000..5efc816
--- /dev/null
+++ b/packages/react/src/makeStyles.test.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+
+import { makeStyles } from './makeStyles';
+
+describe('makeStyles', () => {
+  it('works outside of React components', () => {
+    expect(() => makeStyles({ root: { color: 'red' } })).not.toThrow();
+  });
+
+  it('throws inside React components', () => {
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    jest.spyOn(console, 'error').mockImplementation(() => {});
+
+    const Example: React.FC = () => {
+      makeStyles({ root: { color: 'red' } });
+      return null;
+    };
+    const container = document.createElement('div');
+
+    expect(() => ReactDOM.render(<Example />, container)).toThrow(/All makeStyles\(\) calls should be top level/);
+  });
+});
diff --git a/packages/react/src/renderToStyleElements-node.test.tsx b/packages/react/src/renderToStyleElements-node.test.tsx
new file mode 100644
index 0000000..6f66fa1
--- /dev/null
+++ b/packages/react/src/renderToStyleElements-node.test.tsx
@@ -0,0 +1,195 @@
+/*
+ * @jest-environment node
+ */
+
+// 👆 this is intentionally to test in SSR like environment
+
+import { createDOMRenderer } from '@griffel/core';
+import * as prettier from 'prettier';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/server';
+
+import { makeStyles } from './makeStyles';
+import { RendererProvider } from './RendererContext';
+import { renderToStyleElements } from './renderToStyleElements';
+
+expect.addSnapshotSerializer({
+  test(value) {
+    return typeof value === 'string';
+  },
+  print(value) {
+    /**
+     * test function makes sure that value is the guarded type
+     */
+    const _value = value as string;
+
+    return prettier.format(_value, { parser: 'html' }).trim();
+  },
+});
+
+describe('renderToStyleElements', () => {
+  it('supports overrides', () => {
+    const useExampleStyles = makeStyles({
+      root: {
+        color: 'red',
+        ':hover': { color: 'pink' },
+      },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.root} />;
+    };
+
+    const renderer = createDOMRenderer();
+
+    ReactDOM.renderToStaticMarkup(
+      <RendererProvider renderer={renderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+
+    expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
+      <style data-make-styles-bucket="d" data-make-styles-rehydration="true">
+        .fe3e8s9 {
+          color: red;
+        }</style
+      ><style data-make-styles-bucket="h" data-make-styles-rehydration="true">
+        .f14hep94:hover {
+          color: pink;
+        }
+      </style>
+    `);
+  });
+
+  it('handles @at rules', () => {
+    const useExampleStyles = makeStyles({
+      media: {
+        '@media screen and (max-width: 992px)': {
+          ':hover': { color: 'blue' },
+        },
+      },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.media} />;
+    };
+
+    const renderer = createDOMRenderer();
+
+    ReactDOM.renderToStaticMarkup(
+      <RendererProvider renderer={renderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+
+    expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
+      <style data-make-styles-bucket="t" data-make-styles-rehydration="true">
+        @media screen and (max-width: 992px) {
+          .fnao3vb:hover {
+            color: blue;
+          }
+        }
+      </style>
+    `);
+  });
+
+  it('handles keyframes', () => {
+    const useExampleStyles = makeStyles({
+      keyframe: {
+        animationName: {
+          from: {
+            transform: 'rotate(0deg)',
+          },
+          to: {
+            transform: 'rotate(360deg)',
+          },
+        },
+      },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.keyframe} />;
+    };
+
+    const renderer = createDOMRenderer();
+
+    ReactDOM.renderToStaticMarkup(
+      <RendererProvider renderer={renderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+
+    expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
+      <style data-make-styles-bucket="d" data-make-styles-rehydration="true">
+        .f1g6ul6r {
+          -webkit-animation-name: f1q8eu9e;
+          animation-name: f1q8eu9e;
+        }
+        .f1fp4ujf {
+          -webkit-animation-name: f55c0se;
+          animation-name: f55c0se;
+        }</style
+      ><style data-make-styles-bucket="k" data-make-styles-rehydration="true">
+        @-webkit-keyframes f1q8eu9e {
+          from {
+            -webkit-transform: rotate(0deg);
+            -moz-transform: rotate(0deg);
+            -ms-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+          to {
+            -webkit-transform: rotate(360deg);
+            -moz-transform: rotate(360deg);
+            -ms-transform: rotate(360deg);
+            transform: rotate(360deg);
+          }
+        }
+        @-webkit-keyframes f55c0se {
+          from {
+            -webkit-transform: rotate(0deg);
+            -moz-transform: rotate(0deg);
+            -ms-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+          to {
+            -webkit-transform: rotate(-360deg);
+            -moz-transform: rotate(-360deg);
+            -ms-transform: rotate(-360deg);
+            transform: rotate(-360deg);
+          }
+        }
+        @keyframes f1q8eu9e {
+          from {
+            -webkit-transform: rotate(0deg);
+            -moz-transform: rotate(0deg);
+            -ms-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+          to {
+            -webkit-transform: rotate(360deg);
+            -moz-transform: rotate(360deg);
+            -ms-transform: rotate(360deg);
+            transform: rotate(360deg);
+          }
+        }
+        @keyframes f55c0se {
+          from {
+            -webkit-transform: rotate(0deg);
+            -moz-transform: rotate(0deg);
+            -ms-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+          to {
+            -webkit-transform: rotate(-360deg);
+            -moz-transform: rotate(-360deg);
+            -ms-transform: rotate(-360deg);
+            transform: rotate(-360deg);
+          }
+        }
+      </style>
+    `);
+  });
+});