當初在 Figma 上放入太陽月亮切換的晝夜按鈕,另一個前端就開始唉聲嘆氣。
我當然也有先找網路上的範本,但顏色和風格都跟我網站華麗麗的色調不搭,於是就用生成的圖片搭配動畫自己做一個吧!但卻發現網路上也比較少 React+Tailwind 寫好可用的漂亮元件。
那我們開始囉!
我們在 accountSlice.ts
中定義了與使用者帳號相關的狀態,其中就包含了主題偏好設定。
typescript
import { createSlice } from "@reduxjs/toolkit";
// ...const initialState: UserInfoSupa & { email: string } = {
// ... other statessystem_preference_color_mode: "dark",// 預設為暗色模式// ... other states
};
export const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount: (state, action) => {
// ...
state.system_preference_color_mode =
action.payload.system_preference_color_mode;
// ...
},
// 關鍵的 Reducer!setDarkMode: (state, action) => {
state.system_preference_color_mode = action.payload;
},
},
});
export const { setAccount, setDarkMode } = accountSlice.actions;
export default accountSlice.reducer;
重點分析:
initialState
: 在初始狀態中定義了 system_preference_color_mode
,並給予一個預設值(此處為 'dark'
)。setDarkMode
Reducer: 這是一個專門用來更新主題模式的 action。它接收一個 payload(將會是 'light'
或 'dark'
),並直接更新 state。可以先到這裡下載圖片:
或者放入自己的圖片
import TemplateMoon from "@/assets/lightDarkIcon/TemplateMoon.webp";
import TemplateWake from "@/assets/lightDarkIcon/TemplateWake.webp";
interface ToggleProps {
onClick?: () => void;
isNight?: boolean; // 由外部控制是否為 night 模式
}
export default function ModeToggle({ onClick, isNight = false }: ToggleProps) {
return (
<>
<style>{`
@keyframes twinkle {
0% { opacity: 1; }
100% { opacity: 0.5; }
}
`}</style>
<div
className={`
relative h-8 w-24 cursor-pointer rounded-full
overflow-hidden transition-all duration-300 ease-in-out
hover:scale-105 active:scale-95 focus:outline-none
${
isNight
? "bg-[linear-gradient(145deg,_#1d1f2b,_#16182a)]"
: "bg-[linear-gradient(145deg,_#357bb3,_#2a6290)]"
}
`}
onClick={onClick}
>
{/* Stars */}
<div
className={`
absolute inset-0 transition-all duration-500 ease-in-out
${isNight ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-full"}
`}
>
<div className="absolute left-[10%] top-[50%] h-1.5 w-1.5 animate-[twinkle_1s_infinite_alternate] rounded-full bg-slate-300 shadow-[0_0_4px_#fff]" />
<div className="absolute left-[40%] top-[10%] h-1.5 w-1.5 animate-[twinkle_1s_infinite_alternate] rounded-full bg-slate-300 shadow-[0_0_4px_#fff] rotate-[75deg] scale-110 [animation-delay:300ms]" />
<div className="absolute left-[40%] top-[60%] h-1.5 w-1.5 animate-[twinkle_1s_infinite_alternate] rounded-full bg-slate-300 shadow-[0_0_4px_#fff] rotate-[150deg] scale-75 [animation-delay:600ms]" />
</div>
{/* Sun/Moon */}
<div
className={`
absolute left-1 top-0 h-8 w-8 rounded-full
transition-all duration-300 ease-in-out
${isNight ? "translate-x-[50px] rotate-360" : "translate-x-0 rotate-0"}
`}
>
<img
src={TemplateWake}
alt="Sun"
className={`h-full w-full rounded-full transition-opacity duration-300 ${
isNight ? "opacity-0" : "opacity-100"
}`}
/>
<img
src={TemplateMoon}
alt="Moon"
className={`absolute top-0 left-0 h-full w-full rounded-full transition-opacity duration-300 ${
isNight ? "opacity-100" : "opacity-0"
}`}
/>
</div>
</div>
</>
);
}
重點分析:
onClick
函式和 isNight
布林值。它本身不包含任何邏輯,只負責渲染。isNight
prop,我們使用 Tailwind CSS 的模板字串語法來動態切換 class。例如,背景顏色、太陽/月亮圖示的位置和可見度都是這樣控制的,並搭配 transition
實現了流暢的動畫效果。把按鈕放在header ,從父元件寫功能
// ... imports
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store";
import { setDarkMode } from "@/store/slices/accountSlice";
import { usePatchChangeUserInfo } from "@/api";
// ...
export default function Navigator({ setIsShowNotification }: NavigatorProps) {
const account = useSelector((state: RootState) => state.account);
const dispatch = useDispatch();
const { mutate: patchChangeUserInfo } = usePatchChangeUserInfo();
// 從 Redux store 讀取當前模式
const isNight = account.system_preference_color_mode === "dark";
// 處理模式切換的函式
const handleSwitchMode = () => {
const newMode = isNight ? "light" : "dark";
// 1. 派發 action 更新 Redux state
dispatch(setDarkMode(newMode));
// 2. 呼叫 API 將使用者偏好存到後端資料庫
patchChangeUserInfo({
target: "system_preference_color_mode",
value: newMode,
userID: account.user_id,
});
};
return (
<nav>
<ul className="flex gap-3 items-center ...">
{/* ... */}
<li className="max-sm:hidden ...">
<ModeToggle onClick={handleSwitchMode} isNight={isNight} />
</li>
{/* ... */}
</ul>
</nav>
);
}
useSelector
: 我們用 useSelector
hook 從 Redux store 中取得 account
狀態,並計算出 isNight
布林值。useDispatch
: 取得 dispatch
函式,以便後續派發 actions。handleSwitchMode
: 這是核心邏輯所在。當使用者點擊 ModeToggle
時:
'light'
或 'dark'
)。dispatch(setDarkMode(newMode))
來更新 Redux store 中的全域狀態。usePatchChangeUserInfo
hook,它會發送一個 PATCH 請求到後端 API,更新資料庫中該使用者的偏好設定。handleSwitchMode
和 isNight
作為 props 傳遞給 ModeToggle
元件。我們已經可以更新狀態了,但要如何讓整個應用程式的樣式跟著改變呢?在 App.tsx
和 Tailwind CSS 的 dark
模式設定。
typescriptreact
Show full code block
import Layout from "./components/layout/Layout";
import { useSelector } from "react-redux";
import { RootState } from "./store";
// ...
function App() {
// ...
const isDarkMode = useSelector(
(state: RootState) => state.account.system_preference_color_mode
);
// ...
return (
// 在根佈局元件上根據 Redux 狀態動態添加 'dark' class
<Layout className={isDarkMode === "dark" ? "dark" : ""}>
<Outlet />
<ScrollRestoration />
</Layout>
);
}
export default App;
App.tsx
作為應用的根元件,同樣使用 useSelector
來訂閱主題模式的狀態。isDarkMode === "dark" ? "dark" : ""
的結果作為 className
傳遞給 Layout
元件。這意味著當 Redux state 中的模式為 'dark'
時,Layout
元件就會擁有 dark
這個 class,從而觸發 Tailwind CSS 的暗黑模式。好的就是醬~我們明天繼續講對應晝夜的顏色要怎麼設定!