代码片段
主题切换
ts
import { useLocalStorage } from "foxact/use-local-storage"
import { useEffect, useMemo, useSyncExternalStore } from "react"
const query = "(prefers-color-scheme: dark)"
function getSnapshot() {
return window.matchMedia(query).matches
}
function getServerSnapshot(): undefined {
return undefined
}
function subscribe(callback: () => void) {
const matcher = window.matchMedia(query)
matcher.addEventListener("change", callback)
return () => {
matcher.removeEventListener("change", callback)
}
}
function useSystemDark() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}
const themeOptions = ["system", "light", "dark"] as const
export type Theme = (typeof themeOptions)[number]
function isDarkMode(setting?: Theme | null, isSystemDark?: boolean) {
return setting === "dark" || (isSystemDark && setting !== "light")
}
export function useDark(themeKey = "use-dark") {
const [theme, setTheme] = useLocalStorage<Theme>(themeKey, "system")
const isSystemDark = useSystemDark()
const isDark = useMemo(
() => isDarkMode(theme, isSystemDark),
[isSystemDark, theme],
)
const toggleDark = () => {
if (theme === "system") {
setTheme(isSystemDark ? "light" : "dark")
} else {
setTheme("system")
}
}
useEffect(() => {
const isDark = isDarkMode(theme, isSystemDark)
if (isDark) {
document.documentElement.classList.toggle("dark", true)
} else {
document.documentElement.classList.toggle("dark", false)
}
if (
(theme === "dark" && isSystemDark) ||
(theme === "light" && !isSystemDark)
) {
setTheme("system")
}
}, [theme, isSystemDark, setTheme])
return { isDark, toggleDark }
}
tsx
"use client"
import { useDark } from "~/hooks/use-dark"
export function AppearanceSwitch({ className = "" }: { className?: string }) {
const { toggleDark } = useDark()
return (
<button type="button" onClick={toggleDark} className={"flex " + className}>
<div className="i-lucide-sun scale-100 dark:scale-0 transition-transform duration-500 rotate-0 dark:-rotate-90" />
<div className="i-lucide-moon absolute scale-0 dark:scale-100 transition-transform duration-500 rotate-90 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</button>
)
}
ts
/**
* (Optional) Add transition animation to dark mode switch
* Credit to [@hooray](https://github.com/hooray)
* @see https://github.com/vuejs/vitepress/pull/2347
*/
export function transitionDark(
event: MouseEvent,
isDark: boolean,
toggleDark: () => void,
) {
const isAppearanceTransition =
// @ts-expect-error experimental API
document.startViewTransition &&
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
if (!isAppearanceTransition) {
toggleDark()
return
}
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(async () => {
return new Promise((resolve) => {
resolve(toggleDark())
})
})
void transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath: isDark ? [...clipPath].reverse() : clipPath,
},
{
duration: 400,
easing: "ease-out",
pseudoElement: isDark
? "::view-transition-old(root)"
: "::view-transition-new(root)",
},
)
})
}
css
html.dark {
color-scheme: dark;
}
/* Optional */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 9999;
}
.dark::view-transition-old(root) {
z-index: 9999;
}
.dark::view-transition-new(root) {
z-index: 1;
}
处理首次加载闪烁
html
<script>
!(function () {
var e =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches,
t = localStorage.getItem("use-dark") || "system"
;('"dark"' === t || (e && '"light"' !== t)) &&
document.documentElement.classList.toggle("dark", !0)
})()
</script>
tsx
// 同时需要忽略水合错误
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<>
<script
id="change-theme"
dangerouslySetInnerHTML={{
__html: `!(function () {
var e =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches,
t = localStorage.getItem("use-dark") || "system";
('"dark"' === t || (e && '"light"' !== t)) &&
document.documentElement.classList.toggle("dark", !0);
})();`,
}}
></script>
{children}
</>
)
}
屏幕断点标识
tsx
import { useEffect, useState } from "react"
export function TailwindIndicator() {
const [show, setShow] = useState(false)
useEffect(() => {
let timeout: number | null
const handleResize = () => {
if (timeout) clearTimeout(timeout)
setShow(true)
timeout = window.setTimeout(() => {
setShow(false)
}, 2000)
}
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
if (timeout) clearTimeout(timeout)
}
}, [])
if (import.meta.env.PROD) return null
return (
<div
className={`fixed bottom-1 left-1 z-50 h-6 w-6 p-3
font-mono text-xs text-white bg-gray-800 rounded-full
${show ? "flex items-center justify-center" : "hidden"}`}
>
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
)
}