Styled-components 基礎

有種用了就回不去的感覺。

styled component 中的 props

既然它是 Component,那當然也有 props 可以用,直接來示範怎麼用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const Button = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: #e4e4e4;
border: none;
color: black;
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}

${
props => props.isDone && `
background-color: #0e920e66;
color: white;
`
}
`

function TodoItem ({ content, handlebuttonClick }) {
return (
<TodoItemWrapper>
<TodoContent>{content}</TodoContent>
<TodoButtonWrapper>
// 在這裡傳入
<Button isDone={true}>已完成</Button>
<Button onClick={handlebuttonClick}>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}

props

在 styled Component 接收 props 的方式是透過 ${...}${...} 裡面會寫一個 function 來接收 props 參數,你就可以根據它來寫不同的 style 了。

繼承 style

這個跟 class 中的繼承有點類似,你可以先繼承某個 styled component, 再下新的 style 來覆寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Button = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: #e4e4e4;
border: none;
color: black;
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}

${
props =>
props.size === 'XL' ? 'font-size:20px' : 'font-size: 12px'
}
`

// 繼承 Button
const PinkButton = styled(Button)`
background-color: pink;
`

extend

如果要對「React」的 component 重新設定 style

先講一個觀念:

只有 styled component 才吃的到樣式,所以你用的時候一定是放 styled component,不是 react component。

用 React component 包裝起來的 styled components

這種要搭配 props 來傳入 className:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function TodoItem ({ className, children, handlebuttonClick }) {
return (
<TodoItemWrapper className={className}>
<TodoContent>{children}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button onClick={handlebuttonClick}>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}

// 可以想成是這邊產生一個 className
// 你要把這個 className 放到你想套用 style 的 component 上
const TodoItem2 = styled(TodoItem)`
background-color: #d6d6d6;
`

style-component-inside-react-component

  1. 用 style component 來包裝的 React component

跟剛剛反過來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 把要包裝的 react component 傳入
const OrangeButton = styled(TestButton)`
${defaultButton}
border-color: ${({ theme }) => theme.orange};
color: ${({ theme }) => theme.orange};
&:hover {
background-color: ${({ theme }) => theme.orange};
}
`;

// 記得要傳入 className
function TestButton ({ className }) {
return <button className={className}>測試用</button>
}

react-component-insilde-styled-include

Medai query

其實就是直接在 styled 中寫 @media 就行了,不過更好的做法是把 breakpoint 拆出來寫成常數引入會更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 引入對應的斷點
import { MEDIA_TABLET, MEDIA_PC } from './constants/breakpoint'

const Button = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: #e4e4e4;
border: none;
color: black;
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}

${MEDIA_TABLET} {
font-size: 1.2em;
}

${MEDIA_PC} {
font-size: 1.5em;
}

`

media-query

變數的運用

首先要用 <ThemeProvider> 把整個 component 給包住:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ThemeProvider } from 'styled-components';

// 這邊就可以定義要傳進去的 props
const theme = {
blue: 'royalblue',
orange: 'darkorange',
green: 'mediumseagreen',
red: 'palevioletred',
}

ReactDOM.render(
// 傳進去
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>,
document.getElementById('root')
)

接著在其他的 style component 就可以透過 props.theme 來存取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Button = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: transparent;
border: 1px solid ${props => props.theme.blue};
color: ${props => props.theme.blue};
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}
`

const GreenButton = styled(Button)`
border-color: ${props => props.theme.green};
color: ${props => props.theme.green};
`
const OrangeButton = styled(Button)`
border-color: ${props => props.theme.orange};
color: ${props => props.theme.orange};
`
const RedButton = styled(Button)`
border-color: ${props => props.theme.red};
color: ${props => props.theme.red};
`

成果大概就像這樣:

variable

不同的標籤想套用相同樣式

假設我寫了一個 <button> 的 styled component,但今天又想在 <a> 上用一樣的樣式時,不需要重新寫一遍,只要利用 as 就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Button = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: transparent;
border: 1px solid ${props => props.theme.blue};
color: ${props => props.theme.blue};
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}
&:hover {
background-color: ${props => props.theme.blue};
color: white;
}
`

function TestScope () {
return (
<TodoButtonWrapper style={{
marginTop: '20px'
}}>
<Button>Button</Button>
// 用 as 變成希望的標籤
<Button as="a" href="#">Link</Button>
<Button as="a" href="#">Link</Button>
</TodoButtonWrapper>
)
}

change-styled-tag-01

或甚至是變成另一個 component 也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function TestScope () {
return (
<TodoButtonWrapper style={{
marginTop: '20px'
}}>
<Button>Button</Button>
<Button as={ReversedButton}>Button</Button>
<ReversedButton>顛倒文字的按鈕</ReversedButton>

</TodoButtonWrapper>
)
}

function ReversedButton ({ children }) {
return <Button>{children.split('').reverse()}</Button>
}

change-styled-tag-02

自動判斷是不是 HTML 元素的屬性

1
2
3
4
5
6
7
<TodoItemWrapper>
<TodoContent>{todo.content}</TodoContent>
<TodoButtonWrapper>
<GreenButton isDone={todo.isDone} data-id={todo.id} onClick={handleCompletedButtonClick}>{todo.isDone ? '已完成' : '未完成'}</GreenButton>
<RedButton data-id={todo.id} onClick={handleRemoveButtonClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>

以這個例子來說,GreenButton 寫了 isDonedata-id 兩個屬性,但實際上會被渲染出來的只有 data-id,不會有 isDone

html-attr

這就是 style component 會做的自動判斷。

不過有一種情況是可能我想在 style component 傳 id 這個 props:

1
2
3
4
<GreenButton 
id={todo.id}
onClick={handleButtonClick('isCompleted')}>{todo.isDone ? '已完成' : '未完成'}
</GreenButton>

可是又不希望被渲染到 HTML 上,這時候就可以改用 $ 來傳(Transient props):

1
2
3
4
<GreenButton 
$id={todo.id}
onClick={handleButtonClick('isCompleted')}>{todo.isDone ? '已完成' : '未完成'}
</GreenButton>

這樣就不會被渲染了。

所以建議養成一種習慣,只要是給 style component 用的 props 就一律用 $ 來表示,可讀性會更好。

設定屬性值

要在 style component 上設定 HTML 元素的屬性有兩種方式,第一種是直接寫在 Component 上:

1
2
3
4
5
6
7
8
9
10
11
12
const RadioButton = styled.input``

function TestScope () {
return (
<TodoButtonWrapper style={{
marginTop: '20px'
}}>
<RadioButton type="radio" name='gender' value="man"></RadioButton>
<RadioButton type="radio" name='gender' value="female"></RadioButton>
</TodoButtonWrapper>
)
}

第二種是透過 style.attrs 屬性:

1
2
3
4
5
6
7
8
9
10
11
12
const RadioButton = styled.input.attrs({ type: 'radio' })``

function TestScope () {
return (
<TodoButtonWrapper style={{
marginTop: '20px'
}}>
<RadioButton name='gender' value="man"></RadioButton>
<RadioButton name='gender' value="female"></RadioButton>
</TodoButtonWrapper>
)
}

看起來是第二種會好一點,因為只要寫在一個地方就好,之後要改會比較方便。

設定全域空間的樣式

當想要改 <body> 或是 reset 的樣式時,應該就會用到。這是透過 createGlobalStyle 來達成的。

首先要先寫好全域的 style component:

1
2
3
4
5
6
7
8
9
10
11
import { createGlobalStyle } from "styled-components";

export const GlobalStyle = createGlobalStyle`
body {
// 全域空間的 style
background-color: pink;
}
.bg-dark {
background-color: black;
}
`

接著引入到 entry 就會套用了:

1
2
3
4
5
6
7
8
9
ReactDOM.render(
<ThemeProvider theme={theme}>
// 放在這裡,不是外面
// 另外因為是包在 ThemeProvide 裡,所以也能存到 theme 的 props
<GlobalStyle />
<App />
</ThemeProvider>,
document.getElementById('root')
)

定義在 Global 的 className 也可以在底下的 component 使用:

1
2
3
4
5
6
7
8
9
function TestScope () {
return (
<TodoButtonWrapper className="bg-dark">
<Button>123</Button>
<Button>456</Button>
<Button>789</Button>
</TodoButtonWrapper>
)
}

global-style

給選取器更高的權重:&&

先來看段 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

const Span = styled.span`
&& {
color: blue
}
`

const GlobalStyle = createGlobalStyle`
body {
// 全域空間的 style
}
span${Span} {
color: red;
}
`

ReactDOM.render(
<ThemeProvider theme={theme}>
<GlobalStyle />
<App />
<Span>I'm span</Span>
</ThemeProvider>,
document.getElementById('root')
)

簡單來說,我在 Global 宣告 span${Span} 要是紅色,但我希望實際是藍色,所以就在 Span 中用 && 來覆寫全域設定:

higher-specificity

注意 &&& 的差別,一個會被覆寫一個不會。

把會重複使用的樣式存起來

style component 有提供 css 方法讓你把會共用的樣式儲存起來,它的寫法是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { css } from 'styled-components'
// 把共通的樣式用 function 存起來
const buttonDefault = () => css`
border-radius: 4px;
padding: 8px 12px;
background-color: transparent;
cursor: pointer;
flex-shrink: 0;

// 這個寫法會有一些問題,留到下面來解釋
& + & {
margin-left: 8px;
}
`
const BlueButton = styled.button`
// 直接引用
${buttonDefault}
border: 1px solid ${props => props.theme.blue};
color: ${props => props.theme.blue};
&:hover {
background-color: ${props => props.theme.blue};
color: white;
}
`
const GreenButton = styled.button`
// 直接引用
${buttonDefault}
border-color: 'green';
color: 'green';
&:hover {
background-color: 'green';
}
`
const OrangeButton = styled.button`
// 直接引用
${buttonDefault}
border-color: 'orange';
color: 'orange';
&:hover {
background-color: 'orange';
}
`
const RedButton = styled.button`
// 直接引用
${buttonDefault}
border-color: 'red';
color: 'red';
&:hover {
background-color: 'red';
}
`

這樣就能做出這樣的效果:

css-method

不過會發現 & + & 的部分並沒有套用到,為什麼?

這是因為當你建立一個新的 style component 時其實會重新產生一個 className,所以你的 & + & 其實是這樣子:

1
2
3
4
5
6
7
8
9
BlueButton + BlueButton {
margin-left: 8px
}
greenButton + greenButton {
margin-left: 8px
}
RedButton + RedButton {
margin-left: 8px
}

上圖中的綠按鈕旁邊接的是紅按鈕,所以才沒有套用這個規則。

要解決這個問題的辦法有兩種,一種是改用「繼承」的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const BlueButton = styled.button`
border-radius: 4px;
padding: 8px 12px;
background-color: transparent;
border: 1px solid ${props => props.theme.blue};
color: ${props => props.theme.blue};
cursor: pointer;
flex-shrink: 0;

& + & {
margin-left: 8px;
}
&:hover {
background-color: ${props => props.theme.blue};
color: white;
}
`
const GreenButton = styled(BlueButton)`
// ...
`
const OrangeButton = styled(BlueButton)`
// ...
`
const RedButton = styled(BlueButton)`
// ...
`

因為是透過繼承,所以 BlueButton 的部分不會重新產生 className,只有後來新增的才會。

另一種方式是把 & + & 的規則透過父層來指定:

1
2
3
4
5
6
7
8
const TodoButtonWrapper = styled.div`
display: flex;
align-items: center;
// 底下的 button + button 會套用
button + button {
margin-left: 8px;
}
`

至於哪種寫法比較好就見仁見智,我個人是覺得繼承的寫法比較直覺一點,不過缺點是元件本身會多一條限制,因此「可重用性」會比較差。

其他補充

  • style.buttonstyle('button') 是等價的東西
  • 不要把 style component 寫在 React component 裡面,會有效能問題(每次 render 就重新宣告)
JSX 的原理 三種在 React 中寫 CSS 的方式
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×