LoGravel

MUI-React UI Component Library

最後更新:·發布日期:
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;

react-mui-tutorial-disabled

狀態樣式調整

有多種選取樣式的方式,其實跟原生 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。

因為此設定方式較為複雜,這邊會提供使用步驟 :

  1. 從 @mui/material/styles 解構 styled() 使用
  2. 宣告變數,並賦予它為 styled() 函式
  3. styled() 傳入要自定義的 MUI component
  4. 若撰寫為 TypeScript 則需要再次從該元件引入 type 型別,並讓它允許可以傳入的 porps
  5. 最終是傳入一個 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 是允許使用者透過條件去渲染畫面

步驟使用:

  1. 擴展原本的 props 型別(使用 TypeScript 會有此步驟)
  2. styled() 傳入第二個參數 Object,設定 shouldForwardProp 屬性為一個 function,並傳入 prop 參數
  3. 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 預設樣式更改為默認的方式,或新增一系列的色彩供整個專案使用。

更改元件樣式

  1. 從 @mui/material/styles 匯入 createTheme、ThemeProvider 元件
  2. 宣告 theme 常數賦予 createTheme(),傳入一個 Object
  3. object 中寫入想要調整的樣式
  4. 在 App.tsx 最外層使用 ThemeProvider,而 ThemeProvider 可傳入 theme 參數,這邊會將上述設定的 theme 傳入
  5. 這樣就可以正常使用了 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,
    },
  },