Skip to content

Commit

Permalink
feat(flashing): prevent flashing (#29)
Browse files Browse the repository at this point in the history
* feat(flashing): Added `disableTransitionOnThemeChange` prop to ThemeProvider to control the transitions when the theme changes.
---------

Co-authored-by: Federico Di Stefano <[email protected]>
Co-authored-by: Alexandru Bereghici <[email protected]>
  • Loading branch information
3 people committed Feb 2, 2024
1 parent 1f0b273 commit 304822b
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 10 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Let's dig into the details.
- `specifiedTheme`: The theme from the session storage.
- `themeAction`: The action name used to change the theme in the session
storage.
- `disableTransitionOnThemeChange`: Disable CSS transitions on theme change to
prevent the flashing effect.

### useTheme

Expand Down
6 changes: 5 additions & 1 deletion packages/remix-themes-app/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ function App() {
export default function AppWithProviders() {
const data = useLoaderData()
return (
<ThemeProvider specifiedTheme={data.theme} themeAction="/action/set-theme">
<ThemeProvider
specifiedTheme={data.theme}
themeAction="/action/set-theme"
disableTransitionOnThemeChange={true}
>
<App />
</ThemeProvider>
)
Expand Down
4 changes: 4 additions & 0 deletions packages/remix-themes-app/app/styles/index.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
[data-theme='dark'] {
background-color: #000;
color: white;
transition: background-color 2s ease;
}

[data-theme='light'] {
background-color: #f2f2f2;
color: black;
transition: background-color 2s ease;
}

.dark {
background-color: #000;
color: white;
transition: background-color 2s ease;
}

.light {
background-color: #f2f2f2;
color: black;
transition: background-color 2s ease;
}
28 changes: 21 additions & 7 deletions packages/remix-themes/src/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {Dispatch, ReactNode, SetStateAction} from 'react'
import {createContext, useState, useContext, useEffect, useRef} from 'react'
import {useBroadcastChannel} from './useBroadcastChannel'
import {useCorrectCssTransition} from './useCorrectCssTransition'

export enum Theme {
DARK = 'dark',
Expand All @@ -15,6 +16,7 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
ThemeContext.displayName = 'ThemeContext'

const prefersLightMQ = '(prefers-color-scheme: light)'

const getPreferredTheme = () =>
window.matchMedia(prefersLightMQ).matches ? Theme.LIGHT : Theme.DARK

Expand All @@ -25,13 +27,19 @@ export type ThemeProviderProps = {
children: ReactNode
specifiedTheme: Theme | null
themeAction: string
disableTransitionOnThemeChange?: boolean
}

export function ThemeProvider({
children,
specifiedTheme,
themeAction,
disableTransitionOnThemeChange = false,
}: ThemeProviderProps) {
const ensureCorrectTransition = useCorrectCssTransition({
disableTransitions: disableTransitionOnThemeChange,
})

const [theme, setTheme] = useState<Theme | null>(() => {
// On the server, if we don't have a specified theme then we should
// return null and the clientThemeCode will set the theme for us
Expand All @@ -50,9 +58,11 @@ export function ThemeProvider({

const mountRun = useRef(false)

const broadcastThemeChange = useBroadcastChannel('remix-themes', e =>
setTheme(e.data),
)
const broadcastThemeChange = useBroadcastChannel('remix-themes', e => {
ensureCorrectTransition(() => {
setTheme(e.data)
})
})

useEffect(() => {
if (!mountRun.current) {
Expand All @@ -66,16 +76,20 @@ export function ThemeProvider({
body: JSON.stringify({theme}),
})

broadcastThemeChange(theme)
}, [broadcastThemeChange, theme, themeAction])
ensureCorrectTransition(() => {
broadcastThemeChange(theme)
})
}, [broadcastThemeChange, theme, themeAction, ensureCorrectTransition])

useEffect(() => {
const handleChange = (ev: MediaQueryListEvent) => {
setTheme(ev.matches ? Theme.LIGHT : Theme.DARK)
ensureCorrectTransition(() => {
setTheme(ev.matches ? Theme.LIGHT : Theme.DARK)
})
}
mediaQuery?.addEventListener('change', handleChange)
return () => mediaQuery?.removeEventListener('change', handleChange)
}, [])
}, [ensureCorrectTransition])

return (
<ThemeContext.Provider value={[theme, setTheme]}>
Expand Down
43 changes: 43 additions & 0 deletions packages/remix-themes/src/useCorrectCssTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {useCallback} from 'react'

function withoutTransition(callback: Function) {
const css = document.createElement('style')
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`,
),
)
document.head.appendChild(css)

callback()

setTimeout(() => {
// Calling getComputedStyle forces the browser to redraw
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = window.getComputedStyle(css).transition
document.head.removeChild(css)
}, 0)
}

export function useCorrectCssTransition({
disableTransitions = false,
}: {disableTransitions?: boolean} = {}) {
return useCallback(
(callback: Function) => {
if (disableTransitions) {
withoutTransition(() => {
callback()
})
} else {
callback()
}
},
[disableTransitions],
)
}
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@
source-map-support "^0.5.21"
stream-slice "^0.1.2"

"@remix-run/react@latest":
"@remix-run/react@1.12.0", "@remix-run/react@latest":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-1.12.0.tgz#7ad27e7d152b0980ef09ac851f849cc445a9e51f"
integrity sha512-BokbMOILGJvUvwOsTXAUrucvfFz/SBKVgSHke2+89DZIU7H2Z1UWe5t8wRTfjQnfnSH2tAXCF0QxmVpo1GQ3dg==
Expand All @@ -1538,7 +1538,7 @@
express "^4.17.1"
morgan "^1.10.0"

"@remix-run/[email protected]", "@remix-run/server-runtime@latest":
"@remix-run/[email protected]":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@remix-run/server-runtime/-/server-runtime-1.12.0.tgz#a2072c2e88b948b2a657993574cad01d42cf8cd4"
integrity sha512-7I0165Ns/ffPfCEfuiqD58lMderTn2s/sew1xJ34ONa21mG/7+5T7diHIgxKST8rS3816JPmlwSqUaHgwbmO6Q==
Expand Down

0 comments on commit 304822b

Please sign in to comment.