diff --git a/.changeset/quiet-times-agree.md b/.changeset/quiet-times-agree.md
new file mode 100644
index 00000000000..f397c323028
--- /dev/null
+++ b/.changeset/quiet-times-agree.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+RelativeTime: Prevent hydration errors when the server and client use different time zones
\ No newline at end of file
diff --git a/packages/react/src/RelativeTime/RelativeTime.test.tsx b/packages/react/src/RelativeTime/RelativeTime.test.tsx
index ba9f48430f5..79e9c063a4d 100644
--- a/packages/react/src/RelativeTime/RelativeTime.test.tsx
+++ b/packages/react/src/RelativeTime/RelativeTime.test.tsx
@@ -1,4 +1,7 @@
-import {describe, expect, it} from 'vitest'
+import {act} from 'react'
+import {hydrateRoot, type Root} from 'react-dom/client'
+import {renderToString} from 'react-dom/server'
+import {describe, expect, it, vi} from 'vitest'
import RelativeTime from '.'
import {render} from '@testing-library/react'
import {implementsClassName} from '../utils/testing'
@@ -35,6 +38,42 @@ describe('RelativeTime', () => {
expect(container.textContent).toEqual('server rendered date')
})
+ it('hydrates the fallback without errors when server and client time zones differ', async () => {
+ const date = new Date('2024-03-07T00:30:00.000Z')
+ const relativeTime =
+ const toLocaleDateStringSpy = vi.spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('Mar 7, 2024')
+ const container = document.createElement('div')
+ container.innerHTML = renderToString(relativeTime)
+ document.body.appendChild(container)
+
+ toLocaleDateStringSpy.mockImplementation((_locales, options) =>
+ options?.timeZone === 'UTC' ? 'Mar 7, 2024' : 'Mar 6, 2024',
+ )
+
+ const recoverableErrors: unknown[] = []
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+ let root: Root | undefined
+
+ try {
+ expect(container.firstChild).toHaveTextContent('Mar 7, 2024')
+
+ await act(async () => {
+ root = hydrateRoot(container, relativeTime, {
+ onRecoverableError: error => recoverableErrors.push(error),
+ })
+ })
+
+ expect(recoverableErrors).toEqual([])
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
+ expect(container.firstChild).toHaveTextContent('Mar 7, 2024')
+ } finally {
+ toLocaleDateStringSpy.mockRestore()
+ consoleErrorSpy.mockRestore()
+ await act(async () => root?.unmount())
+ container.remove()
+ }
+ })
+
it('does not render no-title attribute by default', () => {
const date = new Date('2024-03-07T12:22:48.123Z')
const {container} = render()
diff --git a/packages/react/src/RelativeTime/RelativeTime.tsx b/packages/react/src/RelativeTime/RelativeTime.tsx
index b42612beef2..d1b55856835 100644
--- a/packages/react/src/RelativeTime/RelativeTime.tsx
+++ b/packages/react/src/RelativeTime/RelativeTime.tsx
@@ -4,7 +4,12 @@ import {createComponent} from '../utils/create-component'
const RelativeTimeComponent = createComponent(RelativeTimeElement, 'relative-time')
-const localeOptions: Intl.DateTimeFormatOptions = {month: 'short', day: 'numeric', year: 'numeric'}
+const localeOptions: Intl.DateTimeFormatOptions = {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ timeZone: 'UTC',
+} satisfies Intl.DateTimeFormatOptions
function RelativeTime({date, datetime, children, noTitle, ...props}: RelativeTimeProps) {
if (datetime) date = new Date(datetime)
return (