Remix

1. Installation#

Inside your Remix project root directory, install Chakra UI by running either of the following:

npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6 @emotion/server@^11

2. Provider Setup#

To prevent loss of styles we need to do some changes on the server-side and client-side.

We’ll create a context.tsx in the app folder.

// context.tsx
import React, { createContext } from 'react'
export interface ServerStyleContextData {
key: string
ids: Array<string>
css: string
}
export const ServerStyleContext = createContext<ServerStyleContextData[] | null>(null)
export interface ClientStyleContextData {
reset: () => void
}
export const ClientStyleContext = createContext<ClientStyleContextData | null>(null)

Next on the agenda is to create the emotion cache file. To do that, create a new createEmotionCache.ts file in the app folder.

// createEmotionCache.ts
import createCache from '@emotion/cache'
export const defaultCache = createEmotionCache()
export default function createEmotionCache() {
return createCache({ key: 'cha' })
}

After creating the emotion cache, we need to modify the entry files for both the client and the server. We'll use our createEmotionCache function here.

// entry.client.tsx
import React, { useState } from 'react'
import * as ReactDOM from 'react-dom/client'
import { CacheProvider } from '@emotion/react'
import { RemixBrowser } from '@remix-run/react'
import { ClientStyleContext } from './context'
import createEmotionCache, { defaultCache } from './createEmotionCache'
interface ClientCacheProviderProps {
children: React.ReactNode;
}
function ClientCacheProvider({ children }: ClientCacheProviderProps) {
const [cache, setCache] = useState(defaultCache)
function reset() {
setCache(createEmotionCache())
}
return (
<ClientStyleContext.Provider value={{ reset }}>
<CacheProvider value={cache}>{children}</CacheProvider>
</ClientStyleContext.Provider>
)
}
const hydrate = () => {
React.startTransition(() => {
ReactDOM.hydrateRoot(
document,
<ClientCacheProvider>
<RemixBrowser />
</ClientCacheProvider>,
)
})
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate)
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
setTimeout(hydrate, 1)
}
// entry.server.tsx
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 './context'
import createEmotionCache from './createEmotionCache'
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const cache = createEmotionCache()
const { extractCriticalToChunks } = createEmotionServer(cache)
const html = renderToString(
<ServerStyleContext.Provider value={null}>
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>,
)
const chunks = extractCriticalToChunks(html)
const markup = renderToString(
<ServerStyleContext.Provider value={chunks.styles}>
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>,
)
responseHeaders.set('Content-Type', 'text/html')
return new Response(`<!DOCTYPE html>${markup}`, {
status: responseStatusCode,
headers: responseHeaders,
})
}

Inside our root.tsx file we'll create a Document wrapper and then we'll wrap our App with the Document.

// root.tsx
import React, { useContext, useEffect } from 'react'
import { withEmotionCache } from '@emotion/react'
import { ChakraProvider } from '@chakra-ui/react'
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react'
import { MetaFunction, LinksFunction } from '@remix-run/node' // Depends on the runtime you choose
import { ServerStyleContext, ClientStyleContext } from './context'
export const meta: MetaFunction = () => {
return [
{ charSet: 'utf-8' },
{ title: 'New Remix App' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
];
};
export const links: LinksFunction = () => {
return [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com' },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'
},
]
}
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 (
<html lang="en">
<head>
<Meta />
<Links />
{serverStyleData?.map(({ key, ids, css }) => (
<style
key={key}
data-emotion={`${key} ${ids.join(' ')}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
))}
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
);

And then we'll wrap the App just like so:

export default function App() {
return (
<Document>
<ChakraProvider>
<Outlet />
</ChakraProvider>
</Document>
)
}

ChakraProvider Props#

NameTypeDefaultDescription
resetCSSbooleantrueautomatically includes <CSSReset />
themeTheme@chakra-ui/themeoptional custom theme
colorModeManagerStorageManagerlocalStorageManagermanager to persist a users color mode preference in
portalZIndexnumberundefinedcommon z-index to use for Portal

Boom! You're good to go with steps 1 and 2 🚀🚀🚀 However, if you'd love to take it a step further, check out step 3.

3. Optional Setup#

Customizing Theme#

If you intend to customise the default theme object to match your design requirements, you can extend the theme from @chakra-ui/react.

Chakra UI provides an extendTheme function that deep merges the default theme with your customizations.

import { extendTheme, ChakraProvider } from '@chakra-ui/react'
const colors = {
brand: {
900: '#1a365d',
800: '#153e75',
700: '#2a69ac',
},
}
const theme = extendTheme({ colors })
export default function App() {
return (
<Document>
<ChakraProvider theme={theme}>
<Outlet />
</ChakraProvider>
</Document>
)
}

Add colorModeManager#

Remix is server-side rendered, so there will be color mode flashing, because chakra stores color mode in localstorage by default.

We will store color mode value in cookie to tell our app to render it in user color mode.

Here's how to fix it:

  1. Create loader in your root.tsx
// Typescript
// This will return cookies
export const loader: LoaderFunction = async ({ request }) => {
// first time users will not have any cookies and you may not return
// undefined here, hence ?? is necessary
return request.headers.get('cookie') ?? ''
}
  1. Give ChakraProvider cookies from loader
// root.tsx
// In your App function
function getColorMode (cookies: string) {
const match = cookies.match(new RegExp(`(^| )${CHAKRA_COOKIE_COLOR_KEY}=([^;]+)`));
return match == null ? void 0 : match[2];
}
// here we can set the default color mode. If we set it to null,
// there's no way for us to know what is the the user's preferred theme
// so the client will have to figure out and maybe there'll be a flash the first time the user visits us.
const DEFAULT_COLOR_MODE: "dark" | "light" | null = 'dark';
const CHAKRA_COOKIE_COLOR_KEY = "chakra-ui-color-mode";
let cookies = useLoaderData()
// the client get the cookies from the document
// because when we do a client routing, the loader can have stored an outdated value
if (typeof document !== "undefined") {
cookies = document.cookie;
}
// get and store the color mode from the cookies.
// It'll update the cookies if there isn't any and we have set a default value
let colorMode = useMemo(() => {
let color = getColorMode(cookies)
if (!color && DEFAULT_COLOR_MODE) {
cookies += ` ${CHAKRA_COOKIE_COLOR_KEY}=${DEFAULT_COLOR_MODE}`;
color = DEFAULT_COLOR_MODE;
}
return color
}, [cookies]);
[...]
// Add classes to html and body and add colorModeManager to ChakraProvider
return (
<html
lang="en"
{...colorMode
&& {
"data-theme": colorMode,
"style": { colorScheme: colorMode },
}
}
>
<head>
<Meta />
<Links />
{serverStyleData?.map(({ key, ids, css }) => (
<style
key={key}
data-emotion={`${key} ${ids.join(" ")}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
))}
</head>
<body
{...colorMode && {
className: `chakra-ui-${colorMode}`
}}
>
<ChakraProvider
colorModeManager={cookieStorageManagerSSR(cookies)}
theme={theme}
>
{children}
</ChakraProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
)

That's it! Now the theme is correctly server side rendered. You can test it deleting the cookies and disabling JavaScript in the browser.

Notes on TypeScript 🚨#

Please note that when adding Chakra UI to a TypeScript project, a minimum TypeScript version of 4.1.0 is required.

Template#

If you're starting a new project and would like to cut down on configuration time, you can use the official template on how to use Chakra UI with Remix:

Proudly made inNigeria by Segun Adebayo

Deployed by â–² Vercel