Remix Themes

repository·main·Indexed 41 days ago

https://github.com/abereghici/remix-themes

An abstraction for managing themes (such as dark and light mode) in React Router v7 or Remix applications. It handles session persistence, system preference synchronization, and prevents the 'flash of unstyled content' (FOUC) during hydration using the PreventFlashOnWrongTheme component. Includes a ThemeProvider, useTheme hook, and utilities like createThemeSessionResolver and createThemeAction for server-side theme management.

Tokens
4.7K
Snippets
16
Records
21
Agent score
100%

What's inside Remix Themes

  1. Create a theme session resolver

    main

    To persist themes across sessions, you must create a themeSessionResolver using createThemeSessionResolver. This requires a session storage instance (e.g., from createCookieSessionStorage).

    Note on Production: Ensure domain and secure parameters are only set for production environments. Setting them on localhost may prevent Safari from storing the cookie.

    // sessions.server.tsx
    import { createThemeSessionResolver } from "remix-themes";
    import { createCookieSessionStorage } from "react-router";
    
    const sessionStorage = createCookieSessionStorage({
      cookie: {
        name: "__remix-themes",
        path: "/",
        httpOnly: true,
        sameSite: "lax",
        secrets: ["s3cr3t"],
      },
    });
    
    export const themeSessionResolver = createThemeSessionResolver(sessionStorage);
  2. Create the theme action route

    main

    You must create an action route to handle theme changes. This route is responsible for updating the theme in the session storage. The path provided to ThemeProvider's themeAction prop must match the location of this route.

    Example for Route File Naming v2: /routes/action/set-theme.ts or /routes/action.set-theme.ts.

    import { createThemeAction } from "remix-themes";
    import { themeSessionResolver } from "./sessions.server";
    
    export const action = createThemeAction(themeSessionResolver);
  3. Set up ThemeProvider and PreventFlashOnWrongTheme

    main

    To implement theme support in your application, follow these steps:

    1. Loader: In your root.tsx, use the themeSessionResolver to fetch the current theme from the session.
    2. ThemeProvider: Wrap your application with ThemeProvider. Pass the stored theme to specifiedTheme and the path of your theme action route to themeAction.
    3. HTML Attributes: Apply the theme to your <html> element using a data-theme or class attribute based on the value from useTheme.
    4. Prevent Flash: Use PreventFlashOnWrongTheme in the <head> to prevent a flash of the wrong theme during hydration if the theme is missing from the session storage.
    // root.tsx
    import {
      ThemeProvider,
      useTheme,
      PreventFlashOnWrongTheme,
    } from "remix-themes";
    import { themeSessionResolver } from "./sessions.server";
    
    export const loader: LoaderFunction = async ({ request }) => {
      const { getTheme } = await themeSessionResolver(request);
      return {
        theme: getTheme(),
      };
    };
    
    export default function AppWithProviders() {
      const data = useLoaderData();
      return (
        <ThemeProvider specifiedTheme={data.theme} themeAction="/action/set-theme">
          <App />
        </ThemeProvider>
      );
    }
    
    function App() {
      const data = useLoaderData();
      const [theme] = useTheme();
      return (
        <html lang="en" data-theme={theme ?? ""}>
          <head>
            <PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
            {/* ... other head elements ... */}
          </head>
          <body>
            <Outlet />
          </body>
        </html>
      );
    }
  4. Configure Playwright for E2E testing

    main

    The project uses Playwright for end-to-end (E2E) testing. The configuration is located in playwright.config.ts and is optimized for both local development and CI environments.

    Key configuration details:

    • Test Directory: Tests are located in ./e2e.
    • Base URL: The baseURL is dynamically set based on the PORT environment variable (defaulting to http://localhost:3000/).
    • Browser Projects: Tests run against chromium (Desktop Chrome), firefox (Desktop Firefox), and webkit (Desktop Safari).
    • CI Behavior: When process.env.CI is detected, the configuration enables forbidOnly (to prevent accidental test.only commits), sets retries to 2, and limits workers to 1 to ensure stability.
    • Web Server: Playwright automatically manages the application lifecycle using the webServer property. It runs yarn app:dev locally or yarn app:start on CI, ensuring the server is available on the configured PORT before tests begin.
    const config: PlaywrightTestConfig = {
      testDir: "./e2e",
      timeout: 30_000,
      expect: {
        timeout: 5_000,
      },
      fullyParallel: true,
      forbidOnly: !!process.env.CI,
      retries: process.env.CI ? 2 : 0,
      workers: process.env.CI ? 1 : undefined,
      reporter: "html",
      use: {
        actionTimeout: 0,
        baseURL: `http://localhost:${PORT}/`,
        trace: "on-first-retry",
        video: "on",
      },
      projects: [
        { name: "chromium", use: { ...devices["Desktop Chrome"] } },
        { name: "firefox", use: { ...devices["Desktop Firefox"] } },
        { name: "webkit", use: { ...devices["Desktop Safari"] } },
      ],
      webServer: {
        command: process.env.CI
          ? `cross-env PORT=${PORT} yarn app:start`
          : `cross-env PORT=${PORT} yarn app:dev`,
        port: Number(PORT),
        reuseExistingServer: true,
      },
    };
  5. createThemeSessionResolver API reference

    main

    The createThemeSessionResolver function creates a resolver that bridges your session storage with the theme logic.

    Arguments:

    • sessionStorage: A session storage instance (e.g., from createCookieSessionStorage).

    Returns: An object containing a resolver function. The resolver function accepts a request and returns:

    • getTheme: Returns the current theme from session storage.
    • setTheme: Sets a new theme name in session storage.
    • commit: Commits the session storage and returns the Set-Cookie header.
  6. PreventFlashOnWrongTheme component API reference

    main

    The PreventFlashOnWrongTheme component prevents a flash of the incorrect theme during hydration when the server-side theme is unknown (null).

    If the theme is null on the server, it applies the browser's theme to the <html> element using either a data-theme attribute (preferred) or a class attribute.

    Props:

    • ssrTheme: A boolean indicating whether a theme was successfully retrieved from the session storage on the server.
  7. useTheme hook API reference

    main

    The useTheme hook allows you to access and modify the active theme within your components.

    Returns: An array containing [theme, setTheme, metadata]:

    • theme: The name of the active theme.
    • setTheme: A function to update the theme. Passing null sets the theme to the system preference.
    • metadata: An object containing:
      • definedBy: The source of the theme ('USER' or 'SYSTEM').
    const [theme, setTheme, metadata] = useTheme();
    
    // Example: Set to dark mode
    setTheme('dark');
    
    // Example: Revert to system preference
    setTheme(null);
  8. ThemeProvider API reference

    main

    The ThemeProvider component manages the theme state and provides the context for theme switching.

    Props:

    • specifiedTheme: The theme currently stored in the session storage.
    • themeAction: The absolute path to the action route used to change the theme.
    • disableTransitionOnThemeChange: (Boolean) If true, disables CSS transitions on theme change to prevent flashing effects.
  9. Configure SSR mode in React Router

    main

    In the react-router.config.ts file, you can control whether the application uses Server-Side Rendering (SSR) or Single Page Application (SPA) mode using the ssr option. By default, ssr is set to true. To enable SPA mode, set ssr to false.

    import type { Config } from "@react-router/dev/config";
    
    export default {
      // Config options...
      // Server-side render by default, to enable SPA mode set this to `false`
      ssr: true,
    } satisfies Config;
  10. Handle theme switching with createThemeAction

    main

    Use createThemeAction to create a Remix/React Router action that handles theme updates. This action typically works in tandem with a ThemeSessionResolver to persist the user's theme preference in their session when they switch themes via a form submission.

    import { createThemeAction } from "remix-themes";
    
    // In your route file:
    export const action = createThemeAction(themeSessionResolver);
  11. Prevent theme flashing with PreventFlashOnWrongTheme

    main
    PreventFlashOnWrongTheme is a utility used to mitigate the 'flash of unstyled content' (FOUC) that occurs when the server-rendered theme does not match the client-side theme (e.g., when a user has a preference stored in a cookie that the client-side JS hasn't applied yet).