Skip to content

Commit d9d7401

Browse files
committed
Fix navbar theme toggle layout shift
1 parent f7b5aec commit d9d7401

2 files changed

Lines changed: 55 additions & 18 deletions

File tree

src/components/Navbar.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -418,21 +418,49 @@ export function Navbar({ children }: { children: React.ReactNode }) {
418418
const containerRef = React.useRef<HTMLDivElement>(null)
419419

420420
React.useEffect(() => {
421+
const container = containerRef.current
422+
423+
if (!container) {
424+
return
425+
}
426+
421427
const updateContainerHeight = () => {
422-
if (containerRef.current) {
423-
const height = containerRef.current.offsetHeight
424-
document.documentElement.style.setProperty(
425-
'--navbar-height',
426-
`${height}px`,
427-
)
428+
const height = container.offsetHeight
429+
document.documentElement.style.setProperty(
430+
'--navbar-height',
431+
`${height}px`,
432+
)
433+
}
434+
435+
let animationFrameId: number | null = null
436+
const scheduleContainerHeightUpdate = () => {
437+
if (animationFrameId !== null) {
438+
return
428439
}
440+
441+
animationFrameId = window.requestAnimationFrame(() => {
442+
animationFrameId = null
443+
updateContainerHeight()
444+
})
429445
}
430446

431447
updateContainerHeight()
432448

433-
window.addEventListener('resize', updateContainerHeight)
449+
const resizeObserver =
450+
typeof window.ResizeObserver === 'function'
451+
? new window.ResizeObserver(scheduleContainerHeightUpdate)
452+
: null
453+
454+
resizeObserver?.observe(container)
455+
window.addEventListener('resize', scheduleContainerHeightUpdate)
456+
434457
return () => {
435-
window.removeEventListener('resize', updateContainerHeight)
458+
if (animationFrameId !== null) {
459+
window.cancelAnimationFrame(animationFrameId)
460+
}
461+
462+
resizeObserver?.disconnect()
463+
window.removeEventListener('resize', scheduleContainerHeightUpdate)
436464
}
437465
}, [])
438466

src/components/ThemeToggle.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Moon, Sun, SunMoon } from 'lucide-react'
44
import { Button } from '~/ui'
55

66
export function ThemeToggle() {
7-
const { themeMode, toggleMode } = useTheme()
7+
const { themeMode, resolvedTheme, toggleMode } = useTheme()
88

99
const handleToggleMode = (e: React.MouseEvent<HTMLButtonElement>) => {
1010
e.preventDefault()
@@ -18,24 +18,33 @@ export function ThemeToggle() {
1818
const nextLabel =
1919
themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto'
2020

21+
const activeIcon = themeMode === 'auto' ? 'auto' : resolvedTheme
22+
23+
const getIconClassName = (icon: typeof activeIcon) =>
24+
[
25+
'col-start-1 row-start-1 h-3.5 w-3.5 shrink-0 transition-opacity motion-reduce:transition-none',
26+
activeIcon === icon ? 'opacity-100' : 'opacity-0',
27+
].join(' ')
28+
2129
return (
2230
<Button
31+
type="button"
2332
variant="icon"
2433
color="gray"
2534
size="icon-sm"
2635
onClick={handleToggleMode}
2736
aria-label={`Theme: ${label}. Switch to ${nextLabel} mode.`}
2837
title={`Theme: ${label}. Switch to ${nextLabel} mode.`}
29-
className="h-7 w-7 rounded-md p-0"
38+
className="h-7 w-7 shrink-0 rounded-md p-0 leading-none"
3039
>
31-
{themeMode === 'auto' ? (
32-
<SunMoon className="w-3.5 h-3.5" />
33-
) : (
34-
<>
35-
<Sun className="w-3.5 h-3.5 hidden light:block" />
36-
<Moon className="w-3.5 h-3.5 hidden dark:block" />
37-
</>
38-
)}
40+
<span
41+
aria-hidden="true"
42+
className="grid h-3.5 w-3.5 shrink-0 place-items-center"
43+
>
44+
<SunMoon className={getIconClassName('auto')} />
45+
<Sun className={getIconClassName('light')} />
46+
<Moon className={getIconClassName('dark')} />
47+
</span>
3948
</Button>
4049
)
4150
}

0 commit comments

Comments
 (0)