diff --git a/contributors.yml b/contributors.yml
index e71ae938dab..76bd120044d 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -381,3 +381,4 @@
- zachdtaylor
- zainfathoni
- zhe
+- guatedude2
diff --git a/examples/theme-ui/.eslintrc.js b/examples/theme-ui/.eslintrc.js
new file mode 100644
index 00000000000..ced78085f86
--- /dev/null
+++ b/examples/theme-ui/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
+};
diff --git a/examples/theme-ui/.gitignore b/examples/theme-ui/.gitignore
new file mode 100644
index 00000000000..3f7bf98da3e
--- /dev/null
+++ b/examples/theme-ui/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+
+/.cache
+/build
+/public/build
+.env
diff --git a/examples/theme-ui/README.md b/examples/theme-ui/README.md
new file mode 100644
index 00000000000..99bf17112be
--- /dev/null
+++ b/examples/theme-ui/README.md
@@ -0,0 +1,49 @@
+# Example app with [theme-ui](https://theme-ui.com/)
+
+This example features how to use [theme-ui](https://theme-ui.com/) with Remix.
+
+## How this implementation works
+
+Since [theme-ui](https://theme-ui.com/) is derived from [emotion](https://emotion.sh/), this example shows how we can leverage Emotion's server-side-caching to enable [theme-ui](https://theme-ui.com/) server side rendering.
+
+*This implementation was based off [Saas-UI Remix guide](https://www.saas-ui.dev/docs/core/installation/remix-guide).*
+
+### Theme-UI Related files
+
+```
+- app/
+ - styles/
- context.tsx
- createEmotionCache.tsx
+ - entry.client.tsx
+ - entry.server.tsx
+ - root.tsx
+```
+
+1. `context.tsx` - Creates the server and client context.
+2. `createEmotionCache.ts` - Create an instance of [Emotion cache](https://emotion.sh/docs/@emotion/cache).
+4. `entry.client.tsx` - Consumes the emotion cache generated by the server and creates a provider that is then passed on the the app.
+5. `entry.server.tsx` - Create the markup with the styles injected to serve on the server response.
+
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/routes/index.tsx`. The page auto-updates as you edit the file.
+
+## Commands
+
+- `dev`: runs your application on `localhost:3000`
+- `build`: creates the production build version
+- `start`: starts a simple server with the build production code
+
+## Related Links
+
+[Theme-UI](https://theme-ui.com/)
diff --git a/examples/theme-ui/app/entry.client.tsx b/examples/theme-ui/app/entry.client.tsx
new file mode 100644
index 00000000000..cbaa3bb7c0c
--- /dev/null
+++ b/examples/theme-ui/app/entry.client.tsx
@@ -0,0 +1,32 @@
+import React, { useState } from 'react';
+import { hydrate } from 'react-dom';
+import { CacheProvider } from '@emotion/react';
+import { RemixBrowser } from '@remix-run/react';
+
+import { ClientStyleContext } from './styles/context';
+import createEmotionCache from './styles/createEmotionCache';
+
+interface ClientCacheProviderProps {
+ children: React.ReactNode;
+}
+
+function ClientCacheProvider({ children }: ClientCacheProviderProps) {
+ const [cache, setCache] = useState(createEmotionCache());
+
+ function reset() {
+ setCache(createEmotionCache());
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+hydrate(
+
+
+ ,
+ document,
+);
diff --git a/examples/theme-ui/app/entry.server.tsx b/examples/theme-ui/app/entry.server.tsx
new file mode 100644
index 00000000000..e5e2acca873
--- /dev/null
+++ b/examples/theme-ui/app/entry.server.tsx
@@ -0,0 +1,38 @@
+import { renderToString } from 'react-dom/server';
+import { CacheProvider } from '@emotion/react';
+import createEmotionServer from '@emotion/server/create-instance';
+import { RemixServer } from '@remix-run/react';
+import type { EntryContext } from '@remix-run/node'; // Depends on the runtime you choose
+
+import { ServerStyleContext } from './styles/context';
+import createEmotionCache from './styles/createEmotionCache';
+
+export default function handleRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext) {
+ const cache = createEmotionCache();
+ const { extractCriticalToChunks } = createEmotionServer(cache);
+
+ const html = renderToString(
+
+
+
+
+
+ );
+
+ const chunks = extractCriticalToChunks(html);
+
+ const markup = renderToString(
+
+
+
+
+
+ );
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ return new Response(`${markup}`, {
+ status: responseStatusCode,
+ headers: responseHeaders,
+ });
+}
diff --git a/examples/theme-ui/app/root.tsx b/examples/theme-ui/app/root.tsx
new file mode 100644
index 00000000000..2cddc17c20d
--- /dev/null
+++ b/examples/theme-ui/app/root.tsx
@@ -0,0 +1,64 @@
+import React, { useContext, useEffect } from 'react';
+import { withEmotionCache } from '@emotion/react';
+import { ThemeProvider } from '@theme-ui/core';
+import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
+import type { MetaFunction, LinksFunction } from '@remix-run/node'; // Depends on the runtime you choose
+
+import { ServerStyleContext, ClientStyleContext } from './styles/context';
+
+export const meta: MetaFunction = () => ({
+ charset: 'utf-8',
+ title: 'New Remix App',
+ viewport: 'width=device-width,initial-scale=1',
+});
+
+interface DocumentProps {
+ children: React.ReactNode;
+}
+
+const Document = withEmotionCache(({ children }: DocumentProps, emotionCache) => {
+ const serverStyleData = useContext(ServerStyleContext);
+ const clientStyleData = useContext(ClientStyleContext);
+
+ // Only executed on client
+ useEffect(() => {
+ // re-link sheet container
+ emotionCache.sheet.container = document.head;
+ // re-inject tags
+ const tags = emotionCache.sheet.tags;
+ emotionCache.sheet.flush();
+ tags.forEach((tag) => {
+ (emotionCache.sheet as any)._insertTag(tag);
+ });
+ // reset cache to reapply global styles
+ clientStyleData?.reset();
+ }, []);
+
+ return (
+
+
+
+
+ {serverStyleData?.map(({ key, ids, css }) => (
+
+ ))}
+
+
+ {children}
+
+
+
+
+
+ );
+});
+
+export default function App() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/theme-ui/app/routes/index.tsx b/examples/theme-ui/app/routes/index.tsx
new file mode 100644
index 00000000000..871fc6f06ad
--- /dev/null
+++ b/examples/theme-ui/app/routes/index.tsx
@@ -0,0 +1,21 @@
+/** @jsx jsx */
+import React from 'react';
+import {jsx} from '@theme-ui/core';
+import { Link } from "@remix-run/react";
+
+
+export default function Index() {
+ return (
+
+
Welcome to Remix with Emotion Example
+
+ -
+ Jokes
+
+ -
+ Jokes: Error
+
+
+
+ );
+}
diff --git a/examples/theme-ui/app/routes/jokes-error.tsx b/examples/theme-ui/app/routes/jokes-error.tsx
new file mode 100644
index 00000000000..0e45d749e96
--- /dev/null
+++ b/examples/theme-ui/app/routes/jokes-error.tsx
@@ -0,0 +1,3 @@
+export default function JokesError() {
+ throw new Error("This route is no joking with us.");
+}
diff --git a/examples/theme-ui/app/routes/jokes.tsx b/examples/theme-ui/app/routes/jokes.tsx
new file mode 100644
index 00000000000..7c01649ba0c
--- /dev/null
+++ b/examples/theme-ui/app/routes/jokes.tsx
@@ -0,0 +1,14 @@
+/** @jsx jsx */
+import React from 'react';
+import { jsx } from '@theme-ui/core';
+import { Link } from "@remix-run/react";
+
+export default function Jokes() {
+ return (
+
+
Jokes
+
This route works fine.
+
Back to home
+
+ );
+}
diff --git a/examples/theme-ui/app/styles/context.tsx b/examples/theme-ui/app/styles/context.tsx
new file mode 100644
index 00000000000..e64347dfc2b
--- /dev/null
+++ b/examples/theme-ui/app/styles/context.tsx
@@ -0,0 +1,15 @@
+import React, { createContext } from 'react';
+
+export interface ServerStyleContextData {
+ key: string;
+ ids: Array;
+ css: string;
+}
+
+export const ServerStyleContext = createContext(null);
+
+export interface ClientStyleContextData {
+ reset: () => void;
+}
+
+export const ClientStyleContext = createContext(null);
diff --git a/examples/theme-ui/app/styles/createEmotionCache.tsx b/examples/theme-ui/app/styles/createEmotionCache.tsx
new file mode 100644
index 00000000000..37fd756c3ca
--- /dev/null
+++ b/examples/theme-ui/app/styles/createEmotionCache.tsx
@@ -0,0 +1,5 @@
+import createCache from '@emotion/cache';
+
+export default function createEmotionCache() {
+ return createCache({ key: 'css' });
+}
diff --git a/examples/theme-ui/package.json b/examples/theme-ui/package.json
new file mode 100644
index 00000000000..91a1a9a30df
--- /dev/null
+++ b/examples/theme-ui/package.json
@@ -0,0 +1,31 @@
+{
+ "private": true,
+ "sideEffects": false,
+ "scripts": {
+ "build": "remix build",
+ "dev": "remix dev",
+ "start": "remix-serve build"
+ },
+ "dependencies": {
+ "@emotion/cache": "^11.7.1",
+ "@emotion/react": "^11.8.1",
+ "@emotion/server": "^11.4.0",
+ "@remix-run/node": "1.6.2",
+ "@remix-run/react": "1.6.2",
+ "@remix-run/serve": "1.6.2",
+ "@theme-ui/core": "^0.14.6",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "1.6.2",
+ "@remix-run/eslint-config": "1.6.2",
+ "@types/react": "^17.0.39",
+ "@types/react-dom": "^17.0.13",
+ "eslint": "^8.10.0",
+ "typescript": "^4.6.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+}
diff --git a/examples/theme-ui/public/favicon.ico b/examples/theme-ui/public/favicon.ico
new file mode 100644
index 00000000000..8830cf6821b
Binary files /dev/null and b/examples/theme-ui/public/favicon.ico differ
diff --git a/examples/theme-ui/remix.config.js b/examples/theme-ui/remix.config.js
new file mode 100644
index 00000000000..adf2a0b5d3e
--- /dev/null
+++ b/examples/theme-ui/remix.config.js
@@ -0,0 +1,8 @@
+/** @type {import('@remix-run/dev').AppConfig} */
+module.exports = {
+ ignoredRouteFiles: ["**/.*"],
+ // appDirectory: "app",
+ // assetsBuildDirectory: "public/build",
+ // serverBuildPath: "build/index.js",
+ // publicPath: "/build/",
+};
diff --git a/examples/theme-ui/remix.env.d.ts b/examples/theme-ui/remix.env.d.ts
new file mode 100644
index 00000000000..72e2affe311
--- /dev/null
+++ b/examples/theme-ui/remix.env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/examples/theme-ui/sandbox.config.json b/examples/theme-ui/sandbox.config.json
new file mode 100644
index 00000000000..4363d87a30d
--- /dev/null
+++ b/examples/theme-ui/sandbox.config.json
@@ -0,0 +1,6 @@
+{
+ "hardReloadOnChange": true,
+ "container": {
+ "port": 3000
+ }
+}
diff --git a/examples/theme-ui/tsconfig.json b/examples/theme-ui/tsconfig.json
new file mode 100644
index 00000000000..ab2be78ec5a
--- /dev/null
+++ b/examples/theme-ui/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2019"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "@theme-ui/core",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "target": "ES2019",
+ "strict": true,
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Remix takes care of building everything in `remix build`.
+ "noEmit": true
+ }
+}