使用 Tailwind CSS
學習如何使用 Tailwind CSS 設計 MUI Base 組件的樣式。
開始使用
本指南的目標是教您如何在使用 Tailwind CSS 設計 MUI Base 組件樣式的同時,建構互動式且易於存取的應用程式。
先決條件
本指南假設您已具備以下方面的基本知識
- Tailwind CSS
- React 中的 TypeScript
- 建構 React UI 組件
我們將不會在此處詳細介紹這些主題。
本指南的最終成果是一個互動式媒體播放器介面。以下是它最終呈現的樣貌
設定專案
我們將在本指南中使用帶有 TypeScript 的 create-react-app
。建立專案後,請按照 Tailwind CSS 安裝頁面上的指示,以設定 tailwind
。接下來,在專案中安裝 @mui/base
npm install @mui/base
新增播放器標記
現在,建立一個名為 Player.tsx
的檔案,並新增以下包含 Tailwind CSS 類別的標記
Player.tsx
import * as React from 'react';
const Player = React.forwardRef(function Player(
props: { className?: string },
ref: React.ForwardedRef<HTMLDivElement>,
) {
const { className = '', ...other } = props;
return (
<div
className={`max-w-[600px] max-h-[240px] m-auto ${className}`}
{...other}
ref={ref}
>
<div className="bg-white border-slate-100 dark:bg-slate-800 dark:border-slate-500 border-b rounded-t-xl p-4 pb-6 sm:p-10 sm:pb-8 lg:p-6 xl:p-10 xl:pb-8 space-y-6 sm:space-y-8 lg:space-y-6 xl:space-y-8">
<div className="flex items-center space-x-4">
<img
src="https://mui.dev.org.tw/static/base-ui/with-tailwind-css/full-stack-radio.png"
alt=""
width="88"
height="88"
className="flex-none rounded-lg bg-slate-100"
loading="lazy"
/>
<div className="min-w-0 flex-auto space-y-1 font-semibold">
<p className="text-cyan-500 dark:text-cyan-400 text-sm leading-6">
<abbr title="Episode">Ep.</abbr> 128
</p>
<h2 className="text-slate-500 dark:text-slate-400 text-sm leading-6 truncate">
Scaling CSS at Heroku with Utility Classes
</h2>
<p className="text-slate-900 dark:text-slate-50 text-lg">
Full Stack Radio
</p>
</div>
</div>
<div className="space-y-2">
<div className="relative">
<div className="bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className="bg-cyan-500 dark:bg-cyan-400 w-1/2 h-2"
role="progressbar"
aria-label="music progress"
aria-valuenow={1456}
aria-valuemin={0}
aria-valuemax={4550}
></div>
</div>
<div className="ring-cyan-500 dark:ring-cyan-400 ring-2 absolute left-1/2 top-1/2 w-4 h-4 -mt-2 -ml-2 flex items-center justify-center bg-white rounded-full shadow">
<div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"></div>
</div>
</div>
<div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
<div className="text-cyan-500 dark:text-slate-100">24:16</div>
<div className="text-slate-500 dark:text-slate-400">75:50</div>
</div>
</div>
</div>
<div className="bg-slate-50 text-slate-500 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center">
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Add to favorites">
<svg width="24" height="24">
<path
d="M7 6.931C7 5.865 7.853 5 8.905 5h6.19C16.147 5 17 5.865 17 6.931V19l-5-4-5 4V6.931Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="hidden sm:block lg:hidden xl:block"
aria-label="Previous"
>
<svg width="24" height="24" fill="none">
<path
d="m10 12 8-6v12l-8-6Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 6v12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button type="button" aria-label="Rewind 10 seconds">
<svg width="24" height="24" fill="none">
<path
d="M6.492 16.95c2.861 2.733 7.5 2.733 10.362 0 2.861-2.734 2.861-7.166 0-9.9-2.862-2.733-7.501-2.733-10.362 0A7.096 7.096 0 0 0 5.5 8.226"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 5v3.111c0 .491.398.889.889.889H9"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<button
type="button"
className="bg-white text-slate-900 dark:bg-slate-100 dark:text-slate-700 flex-none -my-2 mx-auto w-20 h-20 rounded-full ring-1 ring-slate-900/5 shadow-md flex items-center justify-center"
aria-label="Pause"
>
<svg width="30" height="32" fill="currentColor">
<rect x="6" y="4" width="4" height="24" rx="2" />
<rect x="20" y="4" width="4" height="24" rx="2" />
</svg>
</button>
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Skip 10 seconds">
<svg width="24" height="24" fill="none">
<path
d="M17.509 16.95c-2.862 2.733-7.501 2.733-10.363 0-2.861-2.734-2.861-7.166 0-9.9 2.862-2.733 7.501-2.733 10.363 0 .38.365.711.759.991 1.176"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19 5v3.111c0 .491-.398.889-.889.889H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="hidden sm:block lg:hidden xl:block"
aria-label="Next"
>
<svg width="24" height="24" fill="none">
<path
d="M14 12 6 6v12l8-6Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18 6v12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500"
>
1x
</button>
</div>
</div>
</div>
);
});
export default Player;
接下來,將 Player
組件新增到 App.tsx
檔案中。
App.tsx
import * as React from 'react';
import Player from './Player';
function App() {
return <Player />;
}
export default App;
您現在應該會在頁面上看到渲染的播放器,但該組件尚不具互動性——這將是我們在下一步中涵蓋的內容。
新增互動式滑桿組件
建立 Slider 組件
讓我們開始使用 MUI Base 的 Slider 組件為滑桿賦予生命。首先,建立一個名為 Slider.tsx
的新檔案。將以下程式碼複製並貼到檔案中
Slider.tsx
import * as React from 'react';
import {
Slider as BaseSlider,
SliderThumbSlotProps,
SliderProps,
} from '@mui/base/Slider';
const Slider = React.forwardRef(function Slider(
props: SliderProps,
ref: React.ForwardedRef<HTMLSpanElement>,
) {
return (
<BaseSlider
{...props}
ref={ref}
slotProps={{
thumb: {
className:
'ring-cyan-500 dark:ring-cyan-400 ring-2 w-4 h-4 -mt-1 -ml-2 flex items-center justify-center bg-white rounded-full shadow absolute',
},
root: { className: 'w-full relative inline-block h-2 cursor-pointer' },
rail: {
className:
'bg-slate-100 dark:bg-slate-700 h-2 w-full rounded-full block absolute',
},
track: {
className: 'bg-cyan-500 dark:bg-cyan-400 h-2 absolute rounded-full',
},
}}
/>
);
});
export default Slider;
為了為組件的每個部分指定特定的 Tailwind CSS 實用程式類別,我們正在使用 slotProps
。它們大多數是從原始標記複製而來,並在現在具有互動性後進行了微調。
將滑桿新增到播放器
現在讓我們將 Slider
新增到 Player
組件中
Player.tsx
--- a/src/Player.tsx
+++ b/src/Player.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import Slider from './Slider';
const Player = React.forwardRef(function Player(props: { className?: string }, ref: React.ForwardedRef<HTMLDivElement>) {
const { className = '', ...other } = props;
@@ -21,12 +22,7 @@ const Player = React.forwardRef(function Player(props: { className?: string }, r
</div>
<div className="space-y-2">
<div className="relative">
- <div className="bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
- <div className="bg-cyan-500 dark:bg-cyan-400 w-1/2 h-2" role="progressbar" aria-label="music progress" aria-valuenow={1456} aria-valuemin={0} aria-valuemax={4550}></div>
- </div>
- <div className="ring-cyan-500 dark:ring-cyan-400 ring-2 absolute left-1/2 top-1/2 w-4 h-4 -mt-2 -ml-2 flex items-center justify-center bg-white rounded-full shadow">
- <div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"></div>
- </div>
+ <Slider step={50} defaultValue={1456} max={4550} min={0} />
</div>
<div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
<div className="text-cyan-500 dark:text-slate-100">24:16</div>
您應該會看到這樣

自訂滑桿拇指
即使滑桿現在具有互動性,它看起來仍然與原始設計不完全相同。這是因為我們尚未定義代表拇指內點的元素。
為了做到這一點,僅僅對拇指使用類別是不夠的——我們還需要渲染一個自訂組件,該組件會傳遞到 Slider
的 slots
prop 中
Slider.tsx
--- a/src/Slider.tsx
+++ b/src/Slider.tsx
@@ -1,6 +1,17 @@
import * as React from 'react';
import { Slider as BaseSlider, SliderThumbSlotProps, SliderProps } from '@mui/base/Slider';
+const Thumb = React.forwardRef(function Thumb(
+ props: SliderThumbSlotProps,
+ ref: React.ForwardedRef<HTMLSpanElement>,
+) {
+ const { ownerState, className = '', children, ...other } = props;
+ return (<span className={`${className} ring-cyan-500 dark:ring-cyan-400 ring-2 w-4 h-4 -mt-1 -ml-2 flex items-center justify-center bg-white rounded-full shadow absolute`} {...other} ref={ref}>
+ <span className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"></span>
+ {children}
+ </span>);
+});
+
const Slider = React.forwardRef(function Slider(
props: SliderProps,
ref: React.ForwardedRef<HTMLSpanElement>,
@@ -8,9 +19,11 @@ const Slider = React.forwardRef(function Slider(
return (<BaseSlider
{...props}
ref={ref}
+ slots={{
+ thumb: Thumb,
+ }}
slotProps={{
root: { className: 'w-full relative inline-block h-2 cursor-pointer' },
- thumb: { className: 'ring-cyan-500 dark:ring-cyan-400 ring-2 w-4 h-4 -mt-1 -ml-2 flex items-center justify-center bg-white rounded-full shadow absolute' },
rail: { className: 'bg-slate-100 dark:bg-slate-700 h-2 w-full rounded-full block absolute' },
track: { className: 'bg-cyan-500 dark:bg-cyan-400 h-2 absolute rounded-full' }
}}
重新整理頁面後,您應該會看到拇指現在看起來與設計完全相同。
上面的程式碼建立了一個自訂組件,其中包含作為拇指所需的所有類別和 props。由於我們希望在拇指內有一個額外的點,因此我們需要在拇指的標記中新增一個新元素:一個 <span>
。請注意,在拇指之後,我們仍然渲染透過 props 傳遞的 children
。這很重要,因為在這種情況下,children
包含一個隱藏的 <input>
元素,這使得拇指可存取。
這只是一個範例,但對於所有 MUI Base 組件,都可以使用這種為每個 slot 建構自訂組件的模式。
此外,每個 slot 都會收到一個 ownerState
物件,其中包含擁有者組件的 props 和 state。如果您想根據某些內部 state 設計組件樣式,這會很有用。
為按鈕新增自訂焦點選擇器
為了完成本指南,讓我們看看如何根據組件的內部 state 新增自訂樣式。我們將建立一個自訂 Button 組件,該組件使用 MUI Base Button 中的 focusVisible
state 來在其周圍套用青色環。
這就是它的外觀

建立一個 Button.tsx
檔案並複製以下程式碼
Button.tsx
import * as React from 'react';
import {
Button as BaseButton,
ButtonOwnerState,
ButtonProps,
} from '@mui/base/Button';
const Button = React.forwardRef(function Button(
props: ButtonProps,
ref: React.ForwardedRef<HTMLButtonElement>,
) {
return (
<BaseButton
{...props}
slotProps={{
root: (state: ButtonOwnerState) => ({
className: `hover:text-cyan-500 transition-colors ${
state.focusVisible ? 'outline-0 ring-2 ring-cyan-500' : ''
}`,
}),
}}
ref={ref}
/>
);
});
export default Button;
請注意,我們正在為 slotProps
內部的根元素使用回呼。這讓我們可以在 focusVisible
為 true 時有條件地套用實用程式類別。
現在,讓我們用新的自訂 Button
組件替換初始標記中的所有按鈕。
Player.tsx
--- a/src/Player.tsx
+++ b/src/Player.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import Button from './Button';
import Slider from './Slider';
const Player = React.forwardRef(function Player(props: { className?: string }, ref: React.ForwardedRef<HTMLDivElement>) {
@@ -32,46 +33,46 @@ const Player = React.forwardRef(function Player(props: { className?: string }, r
</div>
<div className="bg-slate-50 text-slate-500 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center"> <div className="flex-auto flex items-center justify-evenly">
- <button type="button" aria-label="Add to favorites">
+ <Button aria-label="Add to favorites">
<svg width="24" height="24">
<path d="M7 6.931C7 5.865 7.853 5 8.905 5h6.19C16.147 5 17 5.865 17 6.931V19l-5-4-5 4V6.931Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
- </button>
- <button type="button" className="hidden sm:block lg:hidden xl:block" aria-label="Previous">
+ </Button>
+ <Button className="hidden sm:block lg:hidden xl:block" aria-label="Previous">
<svg width="24" height="24" fill="none">
<path d="m10 12 8-6v12l-8-6Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6 6v12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
- </button>
- <button type="button" aria-label="Rewind 10 seconds">
+ </Button>
+ <Button aria-label="Rewind 10 seconds">
<svg width="24" height="24" fill="none">
<path d="M6.492 16.95c2.861 2.733 7.5 2.733 10.362 0 2.861-2.734 2.861-7.166 0-9.9-2.862-2.733-7.501-2.733-10.362 0A7.096 7.096 0 0 0 5.5 8.226" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 5v3.111c0 .491.398.889.889.889H9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
- </button>
+ </Button>
</div>
- <button type="button" className="bg-white text-slate-900 dark:bg-slate-100 dark:text-slate-700 flex-none -my-2 mx-auto w-20 h-20 rounded-full ring-1 ring-slate-900/5 shadow-md flex items-center justify-center" aria-label="Pause">
+ <Button className="bg-white text-slate-900 dark:bg-slate-100 dark:text-slate-700 flex-none -my-2 mx-auto w-20 h-20 rounded-full border-2 border-slate-900/5 shadow-md flex items-center justify-center" aria-label="Pause">
<svg width="30" height="32" fill="currentColor">
<rect x="6" y="4" width="4" height="24" rx="2" />
<rect x="20" y="4" width="4" height="24" rx="2" />
</svg>
- </button>
+ </Button>
<div className="flex-auto flex items-center justify-evenly">
- <button type="button" aria-label="Skip 10 seconds">
+ <Button aria-label="Skip 10 seconds">
<svg width="24" height="24" fill="none">
<path d="M17.509 16.95c-2.862 2.733-7.501 2.733-10.363 0-2.861-2.734-2.861-7.166 0-9.9 2.862-2.733 7.501-2.733 10.363 0 .38.365.711.759.991 1.176" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M19 5v3.111c0 .491-.398.889-.889.889H15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
- </button>
- <button type="button" className="hidden sm:block lg:hidden xl:block" aria-label="Next">
+ </Button>
+ <Button className="hidden sm:block lg:hidden xl:block" aria-label="Next">
<svg width="24" height="24" fill="none">
<path d="M14 12 6 6v12l8-6Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18 6v12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
- </button>
- <button type="button" className="rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500">
+ </Button>
+ <Button className="rounded-lg text-xs leading-6 font-semibold px-2 border-2 border-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500 hover:ring-cyan-500">
1x
- </button>
+ </Button>
</div>
</div>
</div>
某些按鈕上的某些類別略有更改,因此我們有一個一致的焦點指示器。
我們學到了
以下是我們在本指南中涵蓋的內容
✅ 如何使用 Tailwind CSS 實用程式類別來設計 MUI Base 組件的樣式,並使用 slotProps
prop 來鎖定組件內的特定 slots。
✅ 如何在更複雜的自訂場景中為特定 slots 建立自訂組件。我們使用 component
prop 將它們傳遞到父組件中。
✅ 如何使用回呼作為 slotProps
prop 的值,根據擁有者組件的 state 套用條件式樣式。
在 MUI Base with Tailwind CSS 範例專案中取得本指南中使用的所有程式碼。