MUI-React UI Component Library

前言
前端開發不外乎一定會碰到 UI/UX,而前端開發生態圈也提供相當多的 UI Component Library。此篇文章會講述在 React 生態圈最熱門的 MUI 使用方法,從基本安裝到樣式撰寫及 Component 基礎使用,讓讀者可以簡單理解及學習。
基礎安裝指令
因為 Material UI (MUI),是依賴 CSS-in-JS 的方式,所以在安裝上面需要多增加 @emotion/react 及 @emotion/styled 兩個套件,提供 MUI 可以使用 sx 屬性及 styled() 的撰寫方式。
pnpm add @mui/material @emotion/react @emotion/styled
Icons 安裝指令
pnpm add @mui/icons-material
CSS Reset 設定
CSS Reset 是確保不同瀏覽器的基礎樣式的一致性,所以在網頁開發中都會引入一份 reset 檔案。在 MUI 則 provider 一個 component 使用,就是 CSSBaseline,而CSSBaseline他是基於 nomalize.css 的設定,但並不完全遵守 nomalize.css 的設定,則是有額外加入 MUI 自身定義的一個 CSS Reset。
import Button from '@mui/material/Button';
import CSSBaseline from '@mui/material/CssBaseline';
function App() {
return (
<>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Button variant="contained">Hello world</Button>
</>
);
}
export default App;
元件使用
在 MUI 中提供多種 component 使用,每個 component 都有自身的相關屬性可以使用,並額外提供部分 API 作為使用,在初學 MUI 時,可以需要使用特定的 component 時再去官方文件尋找並理解使用方式就可以慢慢記住每個 component。
以下展示部分 component 使用:
import { useState } from 'react';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
import TextField from '@mui/material/TextField';
function Demo() {
const [value, setValue] = useState('');
return (
<Container maxWidth="xs">
<Stack gap={4}>
<Button variant="contained">Hello World</Button>
<TextField
onChange={(e) => setValue(e.target.value)}
value={value}
error={!value}
disabled={!!value}
/>
<Slider />
</Stack>
</Container>
);
}
export default Demo;
Icons 使用
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
function Demo() {
return <AutoAwesomeIcon fontSize="small" color="error" />;
}
export default Demo;
Icons 搭配其他元件使用
在 UI 畫面上最常看到的就是按鈕上面有 Icon 及文字,MUI 也提供簡易的設定方式,讓 Icon 可以快速設定在 Button 上面
import { useState } from 'react';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
import TextField from '@mui/material/TextField';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
function Demo() {
const [value, setValue] = useState('');
return (
<Container maxWidth="xs">
<Stack gap={4}>
<TextField
onChange={(e) => setValue(e.target.value)}
value={value}
error={!value}
disabled={!!value}
/>
<Slider />
{/* 在 Button component 中設定 startIcon 屬性,可以將 Icon 放在文字前面。另外也提供 endIcon */}
<Button startIcon={<AutoAwesomeIcon />} variant="contained">
Submit
</Button>
</Stack>
</Container>
);
}
export default Demo;
自定義樣式
網頁開發中一定會有客製化網站顏色的需求,MUI 當然也提供可自定義的方法。他全部介紹則在官方文件的 Customization 列表中,以下會介紹基本的使用方法。
One-off Customization
MUI provide 每一個 component 使用 sx properties 使用,這可以為當前元件設定一次使用的 CSS 樣式。
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
function Demo() {
return (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
maxWidth="xs"
>
<Stack gap={4}>
<Slider
disabled
sx={{
width: 100,
color: 'success.main',
':hover': {
backgroundColor: 'info.main',
},
'& .MuiSlider-thumb': {
':hover': { backgroundColor: 'yellow' },
backgroundColor: 'red',
},
}}
/>
</Stack>
</Container>
);
}
export default Demo;
- sx 是 MUI CSS-in-JS 語法糖 + theme 橋接器,可以直接使用 success.main 或 primary.main 等使用顏色,包含其他MUI 官方文件預設提供的顏色。
- CSS 選擇器
& -當前 component 的 root class,就是 F12 會看到的 .MuiXXX-root,例如:& .MuiSlider-thumb這個元件底下的 thumb。
狀態類別使用
在使用特定的狀態類別,可以在開發者原始碼看到 Mui-* 的各種狀態,像是 Mui-disabled、Mui-checked、Mui-completed 等其他的,官方文件也列出可支援的類別。
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
function Demo() {
return (
<Container
sx={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}
maxWidth="xs"
>
<Stack gap={4}>
<Slider
{/* 加上 disabled 狀態 */}
disabled
sx={{
width: 100,
color: 'success.main',
}}
/>
</Stack>
</Container>
);
}
export default Demo;

狀態樣式調整
有多種選取樣式的方式,其實跟原生 CSS 選擇器脫離不了太大關係,如果選擇不到指定 DOM 可以去確認自己選擇器是不是寫錯方式,只要記得 & 是當前 component 的 .Mui-root 就可以幫助你很大的忙。
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
function Demo() {
return (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
maxWidth="xs"
>
<Stack gap={4}>
<Slider
disabled
sx={{
width: 100,
color: 'success.main',
'&.Mui-disabled': {
'& .MuiSlider-thumb': {
backgroundColor: 'success.main',
},
},
'&.Mui-disabled .MuiSlider-rail': {
backgroundColor: 'error.main',
},
}}
/>
</Stack>
</Container>
);
}
export default Demo;
Reusable component
上面介紹 one-off component 的使用方式,可是在前端 UI 當中,容易出現需要重複使用的樣式,此時就可以使用 styled() 方式撰寫一個可重復使用的 component。
因為此設定方式較為複雜,這邊會提供使用步驟 :
- 從 @mui/material/styles 解構 styled() 使用
- 宣告變數,並賦予它為 styled() 函式
- styled() 傳入要自定義的 MUI component
- 若撰寫為 TypeScript 則需要再次從該元件引入 type 型別,並讓它允許可以傳入的 porps
- 最終是傳入一個 callback function,回傳一個物件,像是 sx properties 傳入的 Object。而這一個 callback function 可以解構 theme 的名稱,方便取用所有的 MUI theme 設定的參數
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider, { type SliderProps } from '@mui/material/Slider';
import { styled } from '@mui/material/styles';
const CustomSlider = styled(Slider)<SliderProps>(({ theme }) => ({
width: theme.spacing(10),
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
backgroundColor: 'red',
},
},
}));
function Demo() {
return (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
maxWidth="xs"
>
<Stack gap={4}>
<CustomSlider />
<CustomSlider />
</Stack>
</Container>
);
}
export default Demo;
Dynamic CSS
動態 CSS 是允許使用者透過條件去渲染畫面
步驟使用:
- 擴展原本的 props 型別(使用 TypeScript 會有此步驟)
styled()傳入第二個參數 Object,設定 shouldForwardProp 屬性為一個 function,並傳入 prop 參數- 從
styled()第二個 callback function 解構自定義的參數名稱,就可以在最後回傳的 CSS 樣式中使用
| shouldForwardProp 為 MUI 定義需傳入的,這是告訴 MUI 哪一個參數不是傳遞給 HTML 屬性,因為 MUI 提供的 component 都會有 MUI 本身已經定義可傳入的參數,所以設定 shouldForwardProp 可以跟原本 MUI 設定的區分,雖然我自己使用時不會因為沒有寫這段產生錯誤,但明確定義可以更乾淨、更好維護性,也避免未來的衝突。
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Slider, { type SliderProps } from '@mui/material/Slider';
import { styled } from '@mui/material/styles';
// 定義 error 條件
interface CustomSliderProps extends SliderProps {
error?: boolean;
}
const CustomSlider = styled(Slider, {
// 這邊撰寫邏輯為,當 prop 不是 error 都會傳遞給 DOM
shouldForwardProp: (prop) => prop !== 'error',
})<CustomSliderProps>(({ theme, error }) => ({
width: theme.spacing(10),
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
backgroundColor: error ? 'blue' : 'red',
},
},
}));
function Demo() {
return (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
maxWidth="xs"
>
<Stack gap={4}>
{/* component 傳入 error 參數 */}
<CustomSlider error />
</Stack>
</Container>
);
}
export default Demo;
主題元件 (Global)
用於設定 Global styles 的方法,可以將 MUI 提供的 component 預設樣式更改為默認的方式,或新增一系列的色彩供整個專案使用。
更改元件樣式
- 從 @mui/material/styles 匯入 createTheme、ThemeProvider 元件
- 宣告 theme 常數賦予 createTheme(),傳入一個 Object
- object 中寫入想要調整的樣式
- 在 App.tsx 最外層使用 ThemeProvider,而 ThemeProvider 可傳入 theme 參數,這邊會將上述設定的 theme 傳入
- 這樣就可以正常使用了 Themed component
import CSSBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
// 傳入 components,並將 Mui Button component 的預設樣式更改
components: {
MuiButton: {
// 更改元件預設樣式
defaultProps: {
disableRipple: true,
variant: 'contained',
},
},
},
});
function App() {
return (
<>
<ThemeProvider theme={theme}>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Button>Submit</Button>
</ThemeProvider>
</>
);
}
export default App;
樣式覆寫
可將 component 傳入特定屬性時更改樣式,他不限於傳入某一個屬性,像是 Button 元件 MUI 就提供多個可傳入的樣式,當設定有兩個條件時,才會顯示 style 中設定的樣式。而最重要一點是 props 中的屬性值都需要是 MUI 有提供的。
import CSSBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
// 傳入 components,並將 Mui Button component 的預設樣式更改
components: {
MuiButton: {
// 更改元件預設樣式
defaultProps: {
disableRipple: true,
variant: 'contained',
},
styleOverrides: {
root: {
fontSize: '1.5rem',
variants: [
{
// 當元件傳入的參數要符合以下兩項,才會顯示 style 的樣式
props: {
variant: 'outlined',
color: 'secondary',
},
style: {
fontSize: '3rem',
},
},
],
},
},
},
},
});
function App() {
return (
<>
<ThemeProvider theme={theme}>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Button>Submit</Button>
<Button variant="outlined" color="secondary">
Outlined
</Button>
</ThemeProvider>
</>
);
}
export default App;
新增自定樣式
import CSSBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
// 傳入 components,並將 Mui Button component 的預設樣式更改
components: {
MuiButton: {
// 更改元件預設樣式
defaultProps: {
disableRipple: true,
variant: 'contained',
},
styleOverrides: {
root: {
fontSize: '1.5rem',
variants: [
{
// 新增自定義樣式
props: {
variant: 'dashed',
},
style: {
border: '4px dashed red',
},
},
],
},
},
},
},
});
function App() {
return (
<>
<ThemeProvider theme={theme}>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Button>Submit</Button>
{/* 使用自定義樣式 dashed */}
<Button variant="dashed">Dashed</Button>
</ThemeProvider>
</>
);
}
export default App;
// 增加 TypeScript 型別
import '@mui/material';
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
dashed: true;
}
}
全域 CSS
import CSSBaseline from '@mui/material/CssBaseline';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
// 傳入 components,並將 Mui Button component 的預設樣式更改
components: {
MuiButton: {
// 更改元件預設樣式
defaultProps: {
disableRipple: true,
variant: 'contained',
},
styleOverrides: {
root: {
fontSize: '1.5rem',
variants: [
{
props: {
variant: 'outlined',
color: 'secondary',
},
style: {
fontSize: '3rem',
},
},
{
props: {
variant: 'dashed',
},
style: {
border: '4px dashed red',
},
},
],
},
},
},
// CSS 設定,styleOverrides 需要傳入文字寫 CSS 規則
MuiCssBaseline: {
styleOverrides: (theme) => `
h1 {
color: ${theme.palette.success.main}
}
`,
},
},
});
function App() {
return (
<>
<ThemeProvider theme={theme}>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Button>Submit</Button>
<Button variant="outlined" color="secondary">
Outlined
</Button>
<Button variant="dashed">Dashed</Button>
<Typography variant="h1">h1</Typography>
<Typography variant="h2">h2</Typography>
</ThemeProvider>
</>
);
}
export default App;
Dark mode
簡易設定
import CSSBaseline from '@mui/material/CssBaseline';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import {
createTheme,
ThemeProvider,
useColorScheme,
} from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
function ThemeToggle() {
const { mode, setMode } = useColorScheme();
if (!mode) return null;
return (
<RadioGroup
value={mode}
onChange={(e) => setMode(e.target.value as 'system' | 'light' | 'dark')}
>
<FormControlLabel control={<Radio />} value="system" label="System" />
<FormControlLabel control={<Radio />} value="light" label="Light" />
<FormControlLabel control={<Radio />} value="dark" label="Dark" />
</RadioGroup>
);
}
function App() {
return (
<>
<ThemeProvider theme={theme}>
<CSSBaseline />
<ThemeToggle />
</ThemeProvider>
</>
);
}
export default App;
設定 Dark / light 色彩
跟上面的簡易設定差不多,差別在從原本的 boolean 改為 object 去定義自己的色票
import CSSBaseline from '@mui/material/CssBaseline';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import {
createTheme,
ThemeProvider,
useColorScheme,
alpha,
} from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: {
palette: {
primary: {
main: '#1616a9',
},
},
},
light: {
palette: {
primary: {
main: alpha('#285a5a', 0.5),
},
},
},
},
});
function ThemeToggle() {
const { mode, setMode } = useColorScheme();
if (!mode) return null;
return (
<RadioGroup
value={mode}
onChange={(e) => setMode(e.target.value as 'system' | 'light' | 'dark')}
>
<FormControlLabel control={<Radio />} value="system" label="System" />
<FormControlLabel control={<Radio />} value="light" label="Light" />
<FormControlLabel control={<Radio />} value="dark" label="Dark" />
</RadioGroup>
);
}
function App() {
return (
<>
<ThemeProvider theme={theme}>
<CSSBaseline />
<ThemeToggle />
</ThemeProvider>
</>
);
}
export default App;
v5、v6 注意事項
在查相關資料的時候有發現直接寫 palette 跟 colorSchemes 兩種方式,在深入研究後發現 palette 是屬於 MUI v5~v6 版本撰寫方式,colorSchemes 則是 v7 的撰寫方式。
- v5 是「一個主題 + mode」
- v7 是「多個主題集合」
單一 theme,切換深淺色通常要自己做兩份 theme
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
palette: {
mode: 'dark', // or 'light'
primary: {
main: '#4f46e5',
},
secondary: {
main: '#f59e0b',
},
background: {
default: '#0f172a',
paper: '#111827',
},
text: {
primary: '#e5e7eb',
secondary: '#9ca3af',
},
},
});
export default function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
App
</ThemeProvider>
);
}
v7 colorSchemes 寫法
正式支援多主題架構(這才是 v7 核心)
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
colorSchemes: {
light: {
palette: {
primary: { main: '#4f46e5' },
secondary: { main: '#f59e0b' },
background: {
default: '#ffffff',
paper: '#f8fafc',
},
text: {
primary: '#0f172a',
secondary: '#475569',
},
},
},
dark: {
palette: {
primary: { main: '#818cf8' },
secondary: { main: '#fbbf24' },
background: {
default: '#0f172a',
paper: '#111827',
},
text: {
primary: '#e5e7eb',
secondary: '#9ca3af',
},
},
},
},
});
export default function App() {
return (
<ThemeProvider theme={theme} defaultMode="system">
<CssBaseline />
App
</ThemeProvider>
);
}
響應式設計(Responsive Design)
Grid 可以當成一個 parent container 或 child,其使用 container 參數來判斷。而 MUI 的 Grid 使用 12 columns grid system 的概念在排版,不理解的開發者可以去上網搜尋看看。
當前我使用環境為 v7 版本,Grid 使用方式與 v5、v6 方式不相同,在實作的時候需要注意自己的 MUI 版本號
import CSSBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
function App() {
return (
<>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
{/* spacing 設定每個 column 與 row 的間距,支援 rowSpacing 與 columnSpacing */}
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6, xl: 4 }}>
<Button variant="contained" fullWidth>
1
</Button>
</Grid>
<Grid size={{ xs: 12, md: 6, xl: 4 }}>
<Button variant="contained" fullWidth>
2
</Button>
</Grid>
<Grid size={{ xs: 12, md: 6, xl: 4 }}>
<Button variant="contained" fullWidth>
3
</Button>
</Grid>
</Grid>
</>
);
}
export default App;
Grid 自動補齊
將 Child Grid component size 改為 grow 值,它則會自動補齊,如果有連續兩個相同的,則會平均分配空間
import CSSBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
function App() {
return (
<>
{/* 在 App.tsx 檔案中引入使用 */}
<CSSBaseline />
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6, xl: 4 }}>
<Button variant="contained" fullWidth>
1
</Button>
</Grid>
<Grid size="grow">
<Button variant="contained" fullWidth>
2
</Button>
</Grid>
<Grid size="grow">
<Button variant="contained" fullWidth>
3
</Button>
</Grid>
</Grid>
</>
);
}
export default App;
斷點設定(breakpoint)
const theme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
},
});
// 基本斷點
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 768,
lg: 1024,
xl: 1280,
},
},