diff --git a/app/root.tsx b/app/root.tsx
index b560c11d8..ca41d8405 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -4,11 +4,13 @@ import type { LinksFunction } from "react-router"
import { useChangeLanguage } from "remix-i18next/react"
import type { Route } from "./+types/root"
import { LanguageSwitcher } from "./library/language-switcher"
+import { ClientHintCheck, getHints } from "./services/client-hints"
import tailwindcss from "./tailwind.css?url"
-export async function loader({ context }: Route.LoaderArgs) {
+export async function loader({ context, request }: Route.LoaderArgs) {
const { lang, clientEnv } = context
- return { lang, clientEnv }
+ const hints = getHints(request)
+ return { lang, clientEnv, hints }
}
export const links: LinksFunction = () => [{ rel: "stylesheet", href: tailwindcss }]
@@ -20,7 +22,6 @@ export const handle = {
export default function App({ loaderData }: Route.ComponentProps) {
const { lang, clientEnv } = loaderData
useChangeLanguage(lang)
-
return (
<>
@@ -35,6 +36,7 @@ export const Layout = ({ children }: { children: React.ReactNode }) => {
return (
+
diff --git a/app/services/client-hints.tsx b/app/services/client-hints.tsx
new file mode 100644
index 000000000..328134767
--- /dev/null
+++ b/app/services/client-hints.tsx
@@ -0,0 +1,47 @@
+import { getHintUtils } from "@epic-web/client-hints"
+import { clientHint as colorSchemeHint, subscribeToSchemeChange } from "@epic-web/client-hints/color-scheme"
+import { clientHint as reducedMotionHint, subscribeToMotionChange } from "@epic-web/client-hints/reduced-motion"
+import { clientHint as timeZoneHint } from "@epic-web/client-hints/time-zone"
+import { useEffect } from "react"
+import { useRevalidator, useRouteLoaderData } from "react-router"
+import type { Route } from "../+types/root"
+
+export const { getHints, getClientHintCheckScript } = getHintUtils({
+ theme: colorSchemeHint,
+ timeZone: timeZoneHint,
+ reducedMotion: reducedMotionHint,
+ // add other hints here
+})
+
+/**
+ * @public
+ * Utility function used to get the time zone for the current users browser on either the client or the server.
+ * */
+export const getTimeZone = (request?: Request) => getHints(request).timeZone
+
+/**
+ * @public
+ * Utility used to get the client hints for the current users browser.
+ * */
+export function useHints() {
+ const requestInfo = useRouteLoaderData("root")
+ return requestInfo?.hints
+}
+/**
+ * Utility component used to check the client hints on the client and send them to the server.
+ */
+export function ClientHintCheck({ nonce }: { nonce?: string }) {
+ const { revalidate } = useRevalidator()
+ useEffect(() => subscribeToSchemeChange(() => revalidate()), [revalidate])
+ useEffect(() => subscribeToMotionChange(() => revalidate()), [revalidate])
+
+ return (
+
+ )
+}
diff --git a/package.json b/package.json
index 57a3529f2..9c6a309ee 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"postinstall": "pnpm run typegen"
},
"dependencies": {
+ "@epic-web/client-hints": "1.3.5",
"@forge42/seo-tools": "1.3.0",
"@react-router/node": "7.1.5",
"clsx": "2.1.1",
@@ -74,7 +75,7 @@
"typescript": "5.7.3",
"vite": "6.0.11",
"vite-plugin-babel": "1.3.0",
- "vite-plugin-icons-spritesheet": "3.0.0",
+ "vite-plugin-icons-spritesheet": "3.0.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.5",
"vitest-browser-react": "0.0.4"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c6f34ce71..dee5da51c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@epic-web/client-hints':
+ specifier: 1.3.5
+ version: 1.3.5
'@forge42/seo-tools':
specifier: 1.3.0
version: 1.3.0(typescript@5.7.3)
@@ -149,8 +152,8 @@ importers:
specifier: 1.3.0
version: 1.3.0(@babel/core@7.26.7)(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))
vite-plugin-icons-spritesheet:
- specifier: 3.0.0
- version: 3.0.0(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))
+ specifier: 3.0.1
+ version: 3.0.1(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))
vite-tsconfig-paths:
specifier: 5.1.4
version: 5.1.4(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))
@@ -473,6 +476,9 @@ packages:
'@emotion/weak-memoize@0.4.0':
resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
+ '@epic-web/client-hints@1.3.5':
+ resolution: {integrity: sha512-tFIDxdU5NzN5Ak4gcDOPKkj6aF/qNMC0G+K58CTBZIx7CMSjCrxqhuiEbZBKGDAGJcsQLF5uKKlgs6mgqWmB7Q==}
+
'@esbuild-plugins/node-globals-polyfill@0.2.3':
resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
peerDependencies:
@@ -3565,8 +3571,8 @@ packages:
'@babel/core': ^7.0.0
vite: ^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
- vite-plugin-icons-spritesheet@3.0.0:
- resolution: {integrity: sha512-7DWnUZQ6K9gd3sClurb4OEVn+l4nO5Q8NgwbkWwBbHWrppZPhESMiXxsnTqNbUCjwhDlpgMHbnyZBN1IPFE+fA==}
+ vite-plugin-icons-spritesheet@3.0.1:
+ resolution: {integrity: sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA==}
peerDependencies:
vite: '>=5.2.0'
@@ -4171,6 +4177,8 @@ snapshots:
'@emotion/weak-memoize@0.4.0': {}
+ '@epic-web/client-hints@1.3.5': {}
+
'@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)':
dependencies:
esbuild: 0.17.19
@@ -7035,7 +7043,7 @@ snapshots:
'@babel/core': 7.26.7
vite: 6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)
- vite-plugin-icons-spritesheet@3.0.0(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)):
+ vite-plugin-icons-spritesheet@3.0.1(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)):
dependencies:
chalk: 5.4.1
glob: 11.0.1
diff --git a/vite.config.ts b/vite.config.ts
index eb560b591..d882529cb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -37,9 +37,6 @@ export default defineConfig({
formatter: "biome",
}),
],
- build: {
- assetsInlineLimit: 0,
- },
server: {
open: true,
// biome-ignore lint/nursery/noProcessEnv: Its ok to use process.env here