# 學習使用 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)