Why Remix needs a different approach
Remix renders HTML on the server and hydrates it on the client. That two-phase lifecycle matters for cookie consent: a script that runs during SSR can't read browser cookies, and a script injected only on the client misses the initial page load.
The consent banner itself is purely client-side (it reads and writes browser cookies), but it needs to load early enough to block tracking scripts before they fire. In Remix, that means placing it in root.tsx so it's present on every route from the first server-rendered response.
Quick setup: script in root.tsx
Add the CookieBeam script to the <head> section of your root layout:
// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<script
src="https://cdn.cookiebeam.com/banner/YOUR_BANNER_ID/default/loader.js"
async
/>
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}Replace YOUR_BANNER_ID with your banner ID. The async attribute means it won't block the initial render.
Streaming SSR considerations
Remix supports streaming responses via defer() and React Suspense. The consent banner handles this correctly because:
- The script tag is in
<head>, which is always sent in the first flush of the streamed response. The banner loads before any deferred content arrives. - Consent state is stored in browser cookies, not in React state. It survives route transitions, deferred data resolution, and error boundaries without re-rendering.
- If a streamed chunk includes a tracking script, CookieBeam's auto-blocking intercepts it as it enters the DOM, regardless of when it arrives in the stream.
Consent-aware loaders
Remix loaders run on the server, where there's no consent state (browser cookies aren't available in loaders by default). Two approaches for server-side consent awareness:
Option 1: Client-only tracking (recommended)
Keep all tracking client-side. Load Google Analytics, Meta Pixel, and other trackers via <script> tags that CookieBeam blocks until consent. Your loaders don't need to know about consent at all.
Option 2: Read consent from the cookie header
If you need server-side consent awareness (rare), you can read the CookieBeam consent cookie in your loader:
// app/routes/some-route.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const cookieHeader = request.headers.get("Cookie") || "";
const hasAnalyticsConsent = cookieHeader.includes('"analytics":true');
// Only fetch personalized data if the visitor consented
const data = hasAnalyticsConsent
? await getPersonalizedContent()
: getDefaultContent();
return json({ data });
}This is useful for server-side A/B tests or personalized content that depends on consent.
Google Consent Mode with Remix
Place the CookieBeam script before any Google scripts in your <head>. CookieBeam sets the default consent state (denied for analytics and ads) before GTM or gtag initializes.
In Remix, the script order in root.tsx determines the execution order. Put the CookieBeam script first:
<head>
<Meta />
<Links />
{/* CookieBeam first: sets consent defaults */}
<script src="https://cdn.cookiebeam.com/banner/YOUR_ID/default/loader.js" async />
{/* GTM second: respects consent state */}
<script dangerouslySetInnerHTML={{ __html: `...GTM snippet...` }} />
</head>Remix-specific gotchas
- Client-side navigation: Remix uses client-side routing after the first page load. The consent banner persists across route changes automatically since it's mounted in the root layout, not in individual routes.
- Error boundaries: If a route throws and Remix renders an
ErrorBoundary, the root layout (including the consent script) is still present. Consent isn't lost during error states. - Prefetching: Remix prefetches links on hover. This triggers loader calls but not script execution, so prefetching doesn't cause consent violations. Scripts only fire when the page actually renders.
- Vite dev server: In development, Remix's Vite server runs on
localhost. CookieBeam treats this as a development domain. The banner appears but consent signals aren't sent to production analytics.