{"id":19237160,"url":"https://github.com/epicweb-dev/client-hints","last_synced_at":"2025-05-15T03:05:54.527Z","repository":{"id":209233169,"uuid":"722321249","full_name":"epicweb-dev/client-hints","owner":"epicweb-dev","description":"Eliminate a flash of incorrect content by using client hints","archived":false,"fork":false,"pushed_at":"2024-12-05T06:32:39.000Z","size":26,"stargazers_count":294,"open_issues_count":1,"forks_count":12,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-06T15:19:13.000Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/epicweb-dev/client-hints","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/epicweb-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-22T22:23:05.000Z","updated_at":"2025-04-18T22:38:08.000Z","dependencies_parsed_at":"2023-11-26T02:23:05.423Z","dependency_job_id":"52d62185-3a64-4396-bdcb-670484e42128","html_url":"https://github.com/epicweb-dev/client-hints","commit_stats":{"total_commits":18,"total_committers":7,"mean_commits":"2.5714285714285716","dds":"0.38888888888888884","last_synced_commit":"3913c0496d5539f5e0ffc7b34748e8dbd511d8f5"},"previous_names":["epicweb-dev/client-hints"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/epicweb-dev%2Fclient-hints","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/epicweb-dev%2Fclient-hints/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/epicweb-dev%2Fclient-hints/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/epicweb-dev%2Fclient-hints/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/epicweb-dev","download_url":"https://codeload.github.com/epicweb-dev/client-hints/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254264765,"owners_count":22041793,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T16:25:05.842Z","updated_at":"2025-05-15T03:05:54.508Z","avatar_url":"https://github.com/epicweb-dev.png","language":"TypeScript","readme":"\u003cdiv\u003e\n  \u003ch1 align=\"center\"\u003e\u003ca href=\"https://npm.im/@epic-web/client-hints\"\u003e💡 @epic-web/client-hints\u003c/a\u003e\u003c/h1\u003e\n  \u003cstrong\u003e\n    Eliminate a flash of incorrect content by using client hints\n  \u003c/strong\u003e\n  \u003cp\u003e\n    Detect the user's device preferences (like time zone and color scheme) and\n    send them to the server so you can server render the correct content for\n    them.\n  \u003c/p\u003e\n\u003c/div\u003e\n\n```\nnpm install @epic-web/client-hints\n```\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca\n    alt=\"Epic Web logo\"\n    href=\"https://www.epicweb.dev\"\n  \u003e\n    \u003cimg\n      width=\"300px\"\n      src=\"https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/257881576-fd66040b-679f-4f25-b0d0-ab886a14909a.png\"\n    /\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n\u003chr /\u003e\n\n\u003c!-- prettier-ignore-start --\u003e\n[![Build Status][build-badge]][build]\n[![MIT License][license-badge]][license]\n[![Code of Conduct][coc-badge]][coc]\n\u003c!-- prettier-ignore-end --\u003e\n\n## The Problem\n\nSometimes your server rendered code needs to know something about the client\nthat the browser doesn't send. For example, the server might need to know the\nuser's preferred language, or whether the user prefers light or dark mode.\n\nFor some of this you should have user preferences which can be persisted in a\ncookie or a database, but you can't do this for first-time visitors. All you can\ndo is guess. Unfortunately, if you guess wrong, you end up with a bad experience\nfor the user.\n\nAnd what often happens is we render HTML that's wrong and then hydrate the\napplication to be interactive with client-side JavaScript that now knows the\nuser preferences and now we know the right thing to render. This is great,\nexcept we've already rendered the wrong thing so by hydrating we cause a shift\nfrom the wrong thing to the right thing which is jarring and can be even a worse\nuser experience than leaving the wrong thing in place (I call this a \"flash of\nincorrect content\"). You'll get an error in the console from React when this\nhappens for this reason.\n\n## The Solution\n\nClient hints are a way to avoid this problem. The\n[standard](https://wicg.github.io/user-preference-media-features-headers/#usage-example)\nfor this is still a work in progress and there is uncertainty when they will\nland in all major browsers we are concerned with supporting. So this is a\n\"ponyfill\" of sorts of a similar feature to the client hints headers proposed to\nthe standard.\n\nThe idea behind the standard is when the browser makes a request, instead of\nresponding to the request immediately, the server instead responds to the client\ninforming it there's a need for certain headers. The client will then repeat the\nrequest with those headers added. The server can then respond with the correct\ncontent.\n\nOur solution is inspired by this, but instead of headers we use cookies (which\ncan actually have a few benefits over headers). The idea is to render some\nJavaScript at the top of the `\u003chead\u003e` of our document before anything else. It's\na small and fast inline script which checks the user's cookies for the expected\nclient hints. If they are not present or if they're outdated, it sets a cookie\nand triggers a reload of the page. Effectively doing the same thing the browser\nwould do with the client hints headers.\n\nThis allows us to server render the right thing for first time visitors without\ntriggering a content layout shift or a flash of incorrect content. After that\nfirst render, the client will have the correct cookies and the server will\nrender the right thing every time thereafter.\n\n[Watch the tip](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift)\non [EpicWeb.dev](https://www.epicweb.dev):\n\n[![Kent smiling with VSCode showing code in the client-hints.tsx file](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/242997340-ede18d0a-c117-4c65-9f1e-a87f262e4ce1.jpg)](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift)\n\n## Usage\n\nThis is how `@epic-web/client-hints` is used in the Epic Stack:\n\n```tsx\nimport { getHintUtils } from '@epic-web/client-hints'\nimport {\n\tclientHint as colorSchemeHint,\n\tsubscribeToSchemeChange,\n} from '@epic-web/client-hints/color-scheme'\nimport { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone'\nimport { useRevalidator } from 'react-router'\nimport * as React from 'react'\nimport { useRequestInfo } from './request-info.ts'\n\nconst hintsUtils = getHintUtils({\n\ttheme: colorSchemeHint,\n\ttimeZone: timeZoneHint,\n\t// add other hints here\n})\n\nexport const { getHints } = hintsUtils\n\nexport function useHints() {\n\tconst requestInfo = useRequestInfo()\n\treturn requestInfo.hints\n}\n\nexport function ClientHintCheck({ nonce }: { nonce: string }) {\n\tconst { revalidate } = useRevalidator()\n\tReact.useEffect(\n\t\t() =\u003e subscribeToSchemeChange(() =\u003e revalidate()),\n\t\t[revalidate],\n\t)\n\n\treturn (\n\t\t\u003cscript\n\t\t\tnonce={nonce}\n\t\t\tdangerouslySetInnerHTML={{\n\t\t\t\t__html: hintsUtils.getClientHintCheckScript(),\n\t\t\t}}\n\t\t/\u003e\n\t)\n}\n```\n\nAnd then the server-side code in the root loader (what powers the\n`useRequestInfo` hook) looks like this:\n\n```tsx\nexport async function loader({ request }: DataFunctionArgs) {\n\treturn {\n\t\t// other stuff here...\n\t\trequestInfo: {\n\t\t\thints: getHints(request),\n\t\t},\n\t}\n}\n```\n\nHints include:\n\n- `@epic-web/client-hints/color-scheme` (also exports `subscribeToSchemeChange`)\n- `@epic-web/client-hints/time-zone`\n- `@epic-web/client-hints/reduced-motion` (also exports\n  `subscribeToMotionChange`)\n\n## FAQ\n\n### Customize cookie name\n\nIf you wish to customize the cookie name, you can simply override it like so:\n\n```tsx\nconst hintsUtils = getHintUtils({\n\ttheme: {\n\t\t...colorSchemeHint,\n\t\tcookieName: 'my-custom-cookie-name',\n\t},\n})\n```\n\nIf you're using one of the `subscribeTo*Change` functions, you'll need to pass\nyour custom cookie name to those as well.\n\n## Custom Hints\n\nIf you have anything custom you'd like to detect, hints are actually pretty\nsimple. Here's the code for the timezone hint:\n\n```ts\nimport { type ClientHint } from '@epic-web/client-hints'\n\nexport const clientHint = {\n\tcookieName: 'CH-time-zone',\n\tgetValueCode: 'Intl.DateTimeFormat().resolvedOptions().timeZone',\n\tfallback: 'UTC',\n} as const satisfies ClientHint\u003cstring\u003e\n```\n\nIf you need to transform the value for some reason (like change it from a string\nto a boolean etc.) then you can use the `transform` method. Here's how the color\nscheme hint uses that:\n\n```ts\nimport { type ClientHint } from '@epic-web/client-hints'\n\nexport const clientHint = {\n\tcookieName: 'CH-prefers-color-scheme',\n\tgetValueCode: `window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'`,\n\tfallback: 'light',\n\ttransform(value) {\n\t\treturn value === 'dark' ? 'dark' : 'light'\n\t},\n} as const satisfies ClientHint\u003c'dark' | 'light'\u003e\n```\n\nThe benefit of doing this is the types for the hint will only ever be\n`'dark' | 'light'` and not `string`.\n\n## License\n\nMIT\n\n\u003c!-- prettier-ignore-start --\u003e\n[build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/client-hints/release.yml?branch=main\u0026logo=github\u0026style=flat-square\n[build]: https://github.com/epicweb-dev/client-hints/actions?query=workflow%3Arelease\n[license-badge]: https://img.shields.io/badge/license-MIT%20License-blue.svg?style=flat-square\n[license]: https://github.com/epicweb-dev/client-hints/blob/main/LICENSE\n[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square\n[coc]: https://kentcdodds.com/conduct\n\u003c!-- prettier-ignore-end --\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fepicweb-dev%2Fclient-hints","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fepicweb-dev%2Fclient-hints","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fepicweb-dev%2Fclient-hints/lists"}