Hướng dẫn làm giao diện tối với Zustand
- Ngày đăng
- Tuan Duc Design
Trong bài viết này mình sẽ hướng dẫn các bạn cách làm giao diện tối với Zustand nhé.
Đầu tiên, hãy tạo dự án của bạn với Next:
npx create-next-app with-zustand-darkmode
Đây là các bước mà mình cấu hình khi tạo dự án Next:
- Would you like to use TypeScript with this project? … No / Yes
- Would you like to use ESLint with this project? … No / Yes
Cài đặt Zustand và TailwindCSS
Zustand là giải pháp nhỏ, gọn, nhanh và dễ dàng mở rộng cho vấn đề quản lý trạng thái theo một các rất "gấu 🐻" đời. Thư viện này có API dễ dùng dựa trên các hooks, không hề gập khuôn theo một kiểu mẫu nhất định.
Với YARN
yarn add zustand && yarn add -D tailwindcss postcss autoprefixer
Với PNPM
pnpm add zustand && pnpm add -D tailwindcss postcss autoprefixer
Với NPM
npm i zustand && npm i -D tailwindcss postcss autoprefixer
Chèn vào dự án
Tạo một tệp có tên Theme.js
bên trong thư mục components của bạn.
import { create } from 'zustand'
import { useEffect, useRef, useLayoutEffect } from 'react'
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
const useSettingTheme = create((set) => ({
theme: '',
setTheme: (theme) => set({ theme }),
}))
function update() {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.remove('light')
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
document.documentElement.classList.add('light')
}
}
export function useTheme() {
let { theme, setTheme } = useSettingTheme()
let initial = useRef(true)
useIsomorphicLayoutEffect(() => {
let theme = localStorage.theme
if (theme === 'light' || theme === 'dark') {
setTheme(theme)
} else {
setTheme('system')
}
}, [])
useIsomorphicLayoutEffect(() => {
if (theme === 'system') {
localStorage.removeItem('theme')
} else if (theme === 'light' || theme === 'dark') {
localStorage.theme = theme
}
if (initial.current) {
initial.current = false
} else {
update()
}
}, [theme])
useEffect(() => {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', update)
function onStorage() {
update()
let theme = localStorage.theme
if (theme === 'light' || theme === 'dark') {
setTheme(theme)
} else {
setTheme('system')
}
}
window.addEventListener('storage', onStorage)
window.addEventListener('load', update)
return () => {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', update)
window.removeEventListener('storage', onStorage)
}
}, [setTheme])
return [theme, setTheme]
}
Để hiển thị chức năng bật tắt giao diện tối, tại thư mục components bạn tạo tiếp một tệp có tên ThemeToggle.js
.
import { useTheme } from './Theme'
let themes = [
{
value: 'light',
label: 'Light',
},
{
value: 'dark',
label: 'Dark',
},
{
value: 'system',
label: 'System',
},
]
function SunIcon({ selected, ...props }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
className={
selected ? 'fill-sky-400/20 stroke-sky-500' : 'stroke-slate-400 dark:stroke-slate-500'
}
/>
<path
d="M12 4v1M17.66 6.344l-.828.828M20.005 12.004h-1M17.66 17.664l-.828-.828M12 20.01V19M6.34 17.664l.835-.836M3.995 12.004h1.01M6 6l.835.836"
className={selected ? 'stroke-sky-500' : 'stroke-slate-400 dark:stroke-slate-500'}
/>
</svg>
)
}
function MoonIcon({ selected, ...props }) {
return (
<svg viewBox="0 0 24 24" fill="none" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.715 15.15A6.5 6.5 0 0 1 9 6.035C6.106 6.922 4 9.645 4 12.867c0 3.94 3.153 7.136 7.042 7.136 3.101 0 5.734-2.032 6.673-4.853Z"
className={selected ? 'fill-sky-400/20' : 'fill-transparent'}
/>
<path
d="m17.715 15.15.95.316a1 1 0 0 0-1.445-1.185l.495.869ZM9 6.035l.846.534a1 1 0 0 0-1.14-1.49L9 6.035Zm8.221 8.246a5.47 5.47 0 0 1-2.72.718v2a7.47 7.47 0 0 0 3.71-.98l-.99-1.738Zm-2.72.718A5.5 5.5 0 0 1 9 9.5H7a7.5 7.5 0 0 0 7.5 7.5v-2ZM9 9.5c0-1.079.31-2.082.845-2.93L8.153 5.5A7.47 7.47 0 0 0 7 9.5h2Zm-4 3.368C5 10.089 6.815 7.75 9.292 6.99L8.706 5.08C5.397 6.094 3 9.201 3 12.867h2Zm6.042 6.136C7.718 19.003 5 16.268 5 12.867H3c0 4.48 3.588 8.136 8.042 8.136v-2Zm5.725-4.17c-.81 2.433-3.074 4.17-5.725 4.17v2c3.552 0 6.553-2.327 7.622-5.537l-1.897-.632Z"
className={selected ? 'fill-sky-500' : 'fill-slate-400 dark:fill-slate-500'}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17 3a1 1 0 0 1 1 1 2 2 0 0 0 2 2 1 1 0 1 1 0 2 2 2 0 0 0-2 2 1 1 0 1 1-2 0 2 2 0 0 0-2-2 1 1 0 1 1 0-2 2 2 0 0 0 2-2 1 1 0 0 1 1-1Z"
className={selected ? 'fill-sky-500' : 'fill-slate-400 dark:fill-slate-500'}
/>
</svg>
)
}
export function ThemeSelect() {
let [theme, setTheme] = useTheme()
return (
<div className="flex items-center justify-between">
<label htmlFor="theme" className="font-normal text-slate-700 dark:text-slate-400">
Switch theme
</label>
<div className="relative flex items-center p-2 font-semibold rounded-lg shadow-sm text-slate-700 ring-1 ring-slate-900/10 dark:bg-slate-600 dark:text-slate-200 dark:ring-0">
<SunIcon className="w-6 h-6 mr-2 dark:hidden" selected={theme !== 'system'} />
<MoonIcon className="hidden w-6 h-6 mr-2 dark:block" selected={theme !== 'system'} />
{theme}
<svg className="w-6 h-6 ml-2 text-slate-400" fill="none">
<path
d="m15 11-3 3-3-3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<select
id="theme"
value={theme}
onChange={(e) => setTheme(e.target.value)}
className="absolute inset-0 w-full h-full opacity-0 appearance-none"
>
{themes.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</div>
)
}
Sau đó bạn tạo các tệp sau trong thư mục pages
_app.js
import '../styles/tailwind.css'
import React from 'react'
export default function App({ Component, pageProps }) {
return (
<>
<div className="flex flex-col">
<main className="flex-1 min-h-screen">
<Component {...pageProps} />
</main>
</div>
</>
)
}
_document.js
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
export default class Document extends NextDocument {
static async getInitialProps(ctx) {
const initialProps = await NextDocument.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html lang="en" className="dark">
<Head>
<script
dangerouslySetInnerHTML={{
__html: `
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.remove('light')
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
document.documentElement.classList.add('light')
}
`,
}}
/>
</Head>
<body className="antialiased bg-white text-slate-500 dark:bg-slate-900 dark:text-slate-400">
<Main />
<NextScript />
</body>
</Html>
)
}
}
index.js
import Head from 'next/head'
import { ThemeSelect } from '../components/ThemeToggle'
export default function IndexPage() {
return (
<>
<Head>
<title>With Zustand Dark Mode Tailwind</title>
</Head>
<div className="relative max-w-5xl py-20 mx-auto sm:py-24 lg:py-32">
<h1 className="text-4xl font-extrabold tracking-tight text-center text-slate-900 dark:text-white sm:text-5xl lg:text-6xl">
With Zustand <span className="text-sky-500 dark:text-sky-400">Dark Mode</span> Tailwind
</h1>
</div>
<div className="relative mx-40 mt-6">
<ThemeSelect />
</div>
</>
)
}
Cuối cùng là các tệp cấu hình để hiển thị css từ Tailwind
tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
tailwind.config.js
module.exports = {
experimental: {
optimizeUniversalDefaults: true,
},
content: ['./pages/**/*.js', './components/**/*.js'],
darkMode: ['class', 'html[class~="dark"]'],
theme: {
extend: {},
},
plugins: [],
}