# 學習使用 Chakra UI 設計網站 ## 課程目標 本教學將帶領學生從零開始建立一個整合 Chakra UI 的 Next.js 專案,學習: - Next.js Pages Router 架構 - TypeScript 配置與型別系統 - Chakra UI v3 整合與自定義主題 - 深色模式實作 - 解決 SSR Hydration 問題 ## 一、建立 Next.js 與 Chakra UI 初始專案 ### 1.1 初始化專案 使用 `create-next-app` 建立專案: ```bash npx create-next-app@latest 2025-fullstack-course-forntend ``` [範例專案](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/chakra-ui) #### 課程使用配置 ```bash ✔ What is your project named? … my-app ✔ Would you like to use the recommended Next.js defaults? › No, customize settings ✔ Would you like to use TypeScript? … Yes ✔ Which linter would you like to use? › ESLint ✔ Would you like to use React Compiler? … Yes ✔ Would you like to use Tailwind CSS? … No ✔ Would you like your code inside a `src/` directory? … Yes ✔ Would you like to use App Router? (recommended) … No ✔ Would you like to use Turbopack? (recommended) … No ✔ Would you like to customize the import alias (`@/*` by default)? … Yes ✔ What import alias would you like configured? … @/* ``` #### 安裝套件 ```bash npm i @chakra-ui/react @emotion/react ``` > v3 已精簡 peer deps;一般情況不需要再手動安裝 @emotion/styled 或 framer-motion。 <br/> ### 1.2 建立並使用 Provider v3 推薦用官方 snippet,會幫你放好 Provider: ```bash npx @chakra-ui/cli snippet add provider ``` 這個指令會在你的專案(通常 `src/components/ui/`)生成 `provider.tsx`,裡面已經接妥 v3 所需設定。 #### 設置框架自訂義設定 ```bash= src/ ├── pages/ # Next.js 頁面路由 │ ├── _app.tsx # 應用程式根組件(全局 Provider) │ ├── _document.tsx # HTML 文檔結構 │ └── index.tsx # 首頁範例 │ ├── components/ # React 組件 │ └── ui/ # Chakra UI 相關組件 │ └── provider.tsx # ChakraProvider 配置 │ ├── fixtures/ # 配置和固定資料 │ └── theme/ # Chakra UI 主題配置 │ ├── index.ts # 主題系統整合 │ ├── colors.ts # 自定義顏色定義 │ └── layer-styles.ts # Layer Styles 定義 └── # 其他源碼目錄 ``` --- **`src/fixtures/theme/color.ts`** - 可以覆蓋 chakra 中的色票設置 ```ts= const colors = { blue: { 100: { value: '#e0f2fe' }, 200: { value: '#bae6fd' }, 300: { value: '#7dd3fc' }, 400: { value: '#38bdf8' }, 500: { value: '#0ea5e9' }, 600: { value: '#0284c7' }, 700: { value: '#0369a1' }, 800: { value: '#075985' }, 900: { value: '#0c4a6e' }, }, }; export default colors; ``` > 這裡僅為教學範例,色彩設定的部分詳情可至官網查詢 [Chakra UI Colors](https://www.chakra-ui.com/docs/theming/colors) --- **`src/fixtures/theme/layer-styles.ts`** - 可以定義一些能重複使用的組合樣式 ```ts= import { defineLayerStyles } from '@chakra-ui/react'; const layerStyles = defineLayerStyles({ // border styles 'border-solid-top': { description: 'border top solid', value: { borderTop: '1px solid', borderColor: 'gray.200', }, }, }); export default layerStyles; ``` --- **`src/fixtures/theme/index.ts`** - 建立框架的設定配置 ```ts= import { createSystem, defaultConfig, defineConfig, SystemConfig, SystemContext, } from '@chakra-ui/react'; import colors from './colors'; import layerStyles from './layer-styles'; const config = defineConfig({ theme: { layerStyles, tokens: { colors, }, }, }) as SystemConfig; export const customSystemConfig: SystemContext = createSystem( defaultConfig, config, ); ``` --- **`src/components/ui/provider.tsx`** - 最後將設定配置引入 provider.tsx 中 ```tsx= import { customSystemConfig } from '@/fixtures/theme'; import { ChakraProvider } from '@chakra-ui/react'; import { ColorModeProvider, type ColorModeProviderProps } from './color-mode'; export function Provider(props: ColorModeProviderProps) { return ( <ChakraProvider value={customSystemConfig}> <ColorModeProvider {...props} /> </ChakraProvider> ); } ``` <br/> ### 1.3 建立 Color Mode 組件 以下的指令會自動在 `src/components/ui/` 新增 `color-mode.tsx` 的檔案,並同時完成基礎設置(含 `next-themes` 的 ColorModeProvider)。 ```bash npx @chakra-ui/cli snippet add color-mode ``` #### `src/components/ui/` 底下會自動多出檔案 ```bash= src/ ├── pages/ # Next.js 頁面路由 │ ├── _app.tsx # 應用程式根組件(全局 Provider) │ ├── _document.tsx # HTML 文檔結構 │ └── index.tsx # 首頁範例 │ ├── components/ # React 組件 │ └── ui/ # Chakra UI 相關組件 │ ├── provider.tsx # ChakraProvider 配置 │ └── color-mode.tsx # 深色模式組件和 Hooks │ ├── fixtures/ # 配置和固定資料 │ └── theme/ # Chakra UI 主題配置 │ ├── index.ts # 主題系統整合 │ ├── colors.ts # 自定義顏色定義 │ └── layer-styles.ts # Layer Styles 定義 └── # 其他源碼目錄 ``` #### 如若有設置比較嚴格的 typescript 檢查機制,可先加上註解來處理錯誤訊息 ```tsx= // eslint-disable-next-line @typescript-eslint/no-empty-object-type ``` 安裝下來的檔案會使用繼承的方式讓我們方便進行擴充,但是在比較嚴格的檢查機制下,他將會因為沒有給予內容而報出錯誤訊息 ```tsx= import type { IconButtonProps, SpanProps } from '@chakra-ui/react'; import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react'; import type { ThemeProviderProps } from 'next-themes'; import { ThemeProvider, useTheme } from 'next-themes'; import * as React from 'react'; import { LuMoon, LuSun } from 'react-icons/lu'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ColorModeProviderProps extends ThemeProviderProps {} export function ColorModeProvider(props: ThemeProviderProps) { return ( <ThemeProvider attribute='class' disableTransitionOnChange {...props} /> ); } export type ColorMode = 'light' | 'dark'; export interface UseColorModeReturn { colorMode: ColorMode; setColorMode: (colorMode: ColorMode) => void; toggleColorMode: () => void; } export function useColorMode(): UseColorModeReturn { const { resolvedTheme, setTheme, forcedTheme } = useTheme(); const colorMode = forcedTheme || resolvedTheme; const toggleColorMode = () => { setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'); }; return { colorMode: colorMode as ColorMode, setColorMode: setTheme, toggleColorMode, }; } export function useColorModeValue<T>(light: T, dark: T) { const { colorMode } = useColorMode(); return colorMode === 'dark' ? dark : light; } export function ColorModeIcon() { const { colorMode } = useColorMode(); return colorMode === 'dark' ? <LuMoon /> : <LuSun />; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface ColorModeButtonProps extends Omit<IconButtonProps, 'aria-label'> {} export const ColorModeButton = React.forwardRef< HTMLButtonElement, ColorModeButtonProps >(function ColorModeButton(props, ref) { const { toggleColorMode } = useColorMode(); return ( <ClientOnly fallback={<Skeleton boxSize='9' />}> <IconButton onClick={toggleColorMode} variant='ghost' aria-label='Toggle color mode' size='sm' ref={ref} {...props} css={{ _icon: { width: '5', height: '5', }, }} > <ColorModeIcon /> </IconButton> </ClientOnly> ); }); export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>( function LightMode(props, ref) { return ( <Span color='fg' display='contents' className='chakra-theme light' colorPalette='gray' colorScheme='light' ref={ref} {...props} /> ); }, ); export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>( function DarkMode(props, ref) { return ( <Span color='fg' display='contents' className='chakra-theme dark' colorPalette='gray' colorScheme='dark' ref={ref} {...props} /> ); }, ); ``` > **重點說明**: > > - `ColorModeProvider`: 整合 `next-themes` 的主題管理 > - `useColorMode`: 提供色彩模式狀態和切換函數的 Hook > - `ColorModeButton`: 使用 `ClientOnly` 包裝,避免 SSR hydration 問題 > - `ClientOnly`: 確保組件只在客戶端渲染,防止服務端與客戶端狀態不一致 <br/> ### 1.4 編輯 \_document.tsx 編輯 `src/pages/_document.tsx`: ```tsx= import { Head, Html, Main, NextScript } from 'next/document'; export default function Document() { return ( <Html lang='zh' suppressHydrationWarning> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); } ``` > **重點**: > > - `lang='zh'`: 設定頁面語言為中文 > - `suppressHydrationWarning`: 抑制 hydration 警告(因為深色模式會在客戶端更新 class) <br/> ### 1.5 編輯 \_app.tsx 編輯 `src/pages/_app.tsx`: ```tsx= import { Provider } from '@/components/ui/provider'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { return ( <Provider> <Component {...pageProps} /> </Provider> ); } ``` > **說明**:`_app.tsx` 包裝所有頁面組件,提供全局 Provider。 <br/> ### 1.6 編輯 index.tsx 編輯 `src/pages/index.tsx`: ```tsx= import { ColorModeButton } from '@/components/ui/color-mode'; import { Button } from '@chakra-ui/react'; export default function Home() { return ( <> <ColorModeButton /> <Button colorPalette='blue'>按鈕</Button> </> ); } ``` > **說明**: > > - 使用 `ColorModeButton` 提供深色模式切換按鈕 > - 使用自定義的 `blue` 色票(定義在 `colors.ts`) <br/> ### 1.7 執行與測試 #### 啟動開發伺服器 ```bash npm run dev ``` #### 開啟瀏覽器 前往 [http://localhost:3000](http://localhost:3000) 你應該會看到: - ☀️/🌙 深色模式切換按鈕 - 藍色的按鈕(使用自定義色票) #### 測試功能 1. **測試深色模式**:點擊切換按鈕,觀察主題切換 2. **測試 SSR**:重新整理頁面,確認沒有 hydration 錯誤 3. **檢查控制台**:確認沒有警告或錯誤 <br/> ### 1.8 進階擴充 [範例專案](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/chakra-ui-expand) #### 擴展 1: 新增更多顏色 編輯 `src/fixtures/theme/colors.ts`: ```tsx= const colors = { blue: { /* ... */ }, green: { 500: { value: '#10b981' }, 600: { value: '#059669' }, }, red: { 500: { value: '#ef4444' }, 600: { value: '#dc2626' }, }, }; ``` 使用: ```tsx= <Button colorPalette='green'>綠色按鈕</Button> <Button colorPalette='red'>紅色按鈕</Button> ``` #### 擴展 2: 新增 Layer Styles 編輯 `src/fixtures/theme/layer-styles.ts`: ```tsx= const layerStyles = defineLayerStyles({ 'border-solid-top': { /* ... */ }, 'card-elevated': { description: '卡片樣式', value: { padding: '4', borderRadius: 'md', boxShadow: 'lg', bg: 'white', _dark: { bg: 'gray.800', }, }, }, }); ``` 使用: ```tsx= <Box layerStyle='card-elevated'>卡片內容</Box> ``` #### 擴展 3: 新增更多頁面 建立 `src/pages/about.tsx`: ```tsx= import { Box, Heading } from '@chakra-ui/react'; export default function About() { return ( <Box p={8}> <Heading>關於頁面</Heading> </Box> ); } ``` 前往 [http://localhost:3000/about](http://localhost:3000/about) 查看。 <br/> ## 二、Chakra UI 常用組件 ### 佈局組件 ```tsx= import { Box, Container, Flex, Grid, Stack, VStack, HStack } from '@chakra-ui/react'; // Box - 最基本的佈局組件 <Box padding={4} bg="gray.100">內容</Box> // Container - 固定寬度容器 <Container maxW="container.lg">內容</Container> // Flex - 彈性佈局 <Flex justify="space-between" align="center"> <Box>左側</Box> <Box>右側</Box> </Flex> // Grid - 網格佈局 <Grid templateColumns="repeat(3, 1fr)" gap={6}> <Box>項目 1</Box> <Box>項目 2</Box> <Box>項目 3</Box> </Grid> // Stack - 堆疊佈局 <VStack gap={4}> {/* 垂直 */} <Box>項目 1</Box> <Box>項目 2</Box> </VStack> <HStack gap={4}> {/* 水平 */} <Box>項目 1</Box> <Box>項目 2</Box> </HStack> ``` ### 文字組件 ```tsx= import { Text, Heading } from '@chakra-ui/react'; // Heading <Heading as="h1" size="2xl">大標題</Heading> <Heading as="h2" size="xl">次標題</Heading> // Text <Text fontSize="lg" fontWeight="bold">粗體文字</Text> <Text color="gray.600">灰色文字</Text> ``` ### 互動組件 ```tsx= import { Button, Input, Textarea, Select, Checkbox, Switch } from '@chakra-ui/react'; // Button <Button colorPalette="blue"> 按鈕 </Button> <Button variant="outline">輪廓按鈕</Button> <Button variant="ghost">幽靈按鈕</Button> // Input <Input placeholder="請輸入..." /> // Textarea <Textarea placeholder="請輸入多行文字..." /> // Checkbox <Checkbox.Root> <Checkbox.HiddenInput /> <Checkbox.Control /> <Checkbox.Label>記住我</Checkbox.Label> </Checkbox.Root> // Switch <Switch.Root> <Switch.HiddenInput /> <Switch.Control /> <Switch.Label /> </Switch.Root> ``` ### 資料展示組件 ```tsx= import { Card, Badge, Tag, Avatar, Image } from '@chakra-ui/react'; // Card (Chakra UI v3) <Card.Root> <Card.Header> <Heading size="md">卡片標題</Heading> </Card.Header> <Card.Body> <Text>卡片內容</Text> </Card.Body> <Card.Footer> <Button>動作</Button> </Card.Footer> </Card.Root> // Badge <Badge colorPalette="green">新</Badge> // Avatar <Avatar.Root> <Avatar.Fallback name='Oshigaki Kisame' /> <Avatar.Image src='https://bit.ly/broken-link' /> </Avatar.Root> // Image <Image src='https://i.pravatar.cc/400?u=1' boxSize='150px' borderRadius='full' fit='cover' alt='Naruto Uzumaki' /> ``` <br/> ## 三、Chakra UI Props 速查 ### 間距(Spacing) ```tsx= // padding <Box p={4}>Box</Box> {/* 所有方向 */} <Box px={4}>Box</Box> {/* 水平 (left + right) */} <Box py={4}>Box</Box> {/* 垂直 (top + bottom) */} <Box pt={4}>Box</Box> {/* top */} <Box pb={4}>Box</Box> {/* bottom */} <Box pl={4}>Box</Box> {/* left */} <Box pr={4}>Box</Box> {/* right */} // margin (同樣的邏輯) <Box m={4} mx={4} my={4} mt={4} mb={4} ml={4} mr={4} /> ``` **數值對照**: - `1` = 0.25rem (4px) - `2` = 0.5rem (8px) - `4` = 1rem (16px) - `8` = 2rem (32px) ### 顏色(Colors) ```tsx= // 文字顏色 <Text color="blue.500">藍色文字</Text> <Text color="gray.600">灰色文字</Text> // 背景顏色 <Box bg="blue.50">淺藍背景</Box> <Box bgColor="red.500">紅色背景</Box> // 邊框顏色 <Box borderColor="gray.200" borderWidth={1}><Text>灰色邊框</Text></Box> ``` ### 尺寸(Size) ```tsx= // width <Box w="100%"> {/* 100% */} <Box w="50vw"> {/* 50% viewport width */} <Box w="200px"> {/* 固定寬度 */} <Box maxW="500px"> {/* 最大寬度 */} // height <Box h="100vh"> {/* 100% viewport height */} <Box h="200px"> {/* 固定高度 */} <Box minH="300px"> {/* 最小高度 */} ``` > 僅範例 ### 響應式設計 ```tsx= // 陣列語法(mobile, tablet, desktop) <Box w={["100%", "80%", "60%"]}> // 物件語法 <Box w={{ base: "100%", md: "80%", lg: "60%" }} fontSize={{ base: "sm", md: "md", lg: "lg" }} > // Breakpoints // base: 0px // sm: 480px // md: 768px // lg: 992px // xl: 1280px // 2xl: 1536px ``` > 僅範例 <br/> ## 四、使用深色模式 ### useColorMode ```tsx= import { useColorMode, useColorModeValue } from '@/components/ui/color-mode'; function MyComponent() { const { colorMode, toggleColorMode } = useColorMode(); return <Button onClick={toggleColorMode}>目前模式: {colorMode}</Button>; } ``` ### useColorModeValue ```tsx= function MyComponent() { const bg = useColorModeValue('white', 'gray.800'); const color = useColorModeValue('black', 'white'); return ( <Box bg={bg} color={color}> 內容 </Box> ); } ``` ### CSS Props 方式 ```tsx= <Box bg='white' _dark={{ bg: 'gray.800' }} color='black' _dark={{ color: 'white' }} > 會根據模式自動切換 </Box> ``` ### 使用 ClientOnly 處理 SSR 混合問題 ```tsx= import { useColorMode, useColorModeValue } from '@/components/ui/color-mode'; import { Box, Button, ClientOnly } from '@chakra-ui/react'; export default function Home() { const { colorMode, toggleColorMode } = useColorMode(); const bg = useColorModeValue('white', 'gray.800'); const color = useColorModeValue('black', 'white'); return ( <ClientOnly fallback={<div>Loading...</div>}> <Box bg={bg} color={color}> <Button onClick={toggleColorMode}> 切換至 {colorMode === 'light' ? '深色' : '淺色'} 模式 </Button> </Box> </ClientOnly> ); } ``` <br/> ## 五、導航列設計練習 [範例專案](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/chakra-ui-navbar) 新增 Navbar.tsx 在組件以下新增導覽列檔案 `src/components/Navbar.tsx`: ```tsx= import { ColorModeButton } from '@/components/ui/color-mode'; import { Box, Button, Flex, HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; const Navbar = () => { const router = useRouter(); return ( <Box as='nav' bg='white' borderBottom='1px' borderColor='gray.200' _dark={{ bg: 'gray.800', borderColor: 'gray.700' }} px={4} py={3} // 在 md 以上顯示,在 base 以下隱藏 display={{ base: 'none', md: 'block' }} > <Flex maxW='container.xl' mx='auto' justify='space-between' align='center' > <Box fontWeight='bold' fontSize='xl'> Logo </Box> <HStack gap={4}> <Button variant='ghost' onClick={() => router.push('/')}> 首頁 </Button> <Button variant='ghost' onClick={() => router.push('/about')}> 關於 </Button> <ColorModeButton /> </HStack> </Flex> </Box> ); }; export default Navbar; ``` --- 將導覽列新增到畫面上 `src/pages/index.tsx`: ```tsx= import Navbar from '@/components/Navbar'; import { useColorMode, useColorModeValue } from '@/components/ui/color-mode'; import { Box, Button, ClientOnly } from '@chakra-ui/react'; export default function Home() { const { colorMode, toggleColorMode } = useColorMode(); const bg = useColorModeValue('white', 'gray.800'); const color = useColorModeValue('black', 'white'); return ( <ClientOnly fallback={<div>Loading...</div>}> <Navbar /> <Box bg={bg} color={color}> <Button onClick={toggleColorMode}> 切換至 {colorMode === 'light' ? '深色' : '淺色'} 模式 </Button> </Box> </ClientOnly> ); } ``` --- 可再新增 about 頁面處理導覽列 `src/pages/about.tsx`: ```tsx= import Navbar from '@/components/Navbar'; import { Box, Heading } from '@chakra-ui/react'; export default function About() { return ( <> <Navbar /> <Box p={8}> <Heading>關於頁面</Heading> </Box> </> ); } ``` <br/> # 整合專案開發,完成全端網站應用 [範例專案](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/chakra-ui-api)